From dfaf37cbba684a9913b79ec68fdf5ad18378c170 Mon Sep 17 00:00:00 2001 From: "Chris Ross (ASP.NET)" Date: Tue, 21 Nov 2017 10:59:34 -0800 Subject: [PATCH] Implement config support #1290 #1879 #2016 #2166 #2167 #2188 --- KestrelHttpServer.sln | 20 +- build/dependencies.props | 3 + samples/SampleApp/SampleApp.csproj | 17 +- samples/SampleApp/Startup.cs | 59 +++- .../SampleApp/appsettings.Development.json | 15 + samples/SampleApp/appsettings.Production.json | 14 + samples/SampleApp/appsettings.json | 6 + samples/SystemdTestApp/Startup.cs | 92 ++++++ samples/SystemdTestApp/SystemdTestApp.csproj | 23 ++ samples/SystemdTestApp/testCert.pfx | Bin 0 -> 2483 bytes src/Kestrel.Core/CoreStrings.resx | 15 + src/Kestrel.Core/EndpointConfiguration.cs | 26 ++ .../Internal/AddressBindContext.cs | 1 - src/Kestrel.Core/Internal/AddressBinder.cs | 63 ++-- .../Internal/CertificateLoader.cs | 99 ++++++ .../Internal/ConfigurationReader.cs | 140 +++++++++ .../Internal/HttpsConnectionAdapter.cs | 35 +-- .../Internal/IDefaultHttpsProvider.cs | 10 - .../Internal/Infrastructure/Constants.cs | 7 +- .../Internal/KestrelServerOptionsSetup.cs | 24 +- .../Internal/LoggerExtensions.cs | 2 +- src/Kestrel.Core/Kestrel.Core.csproj | 3 + .../KestrelConfigurationLoader.cs | 297 ++++++++++++++++++ src/Kestrel.Core/KestrelServer.cs | 11 +- src/Kestrel.Core/KestrelServerOptions.cs | 110 ++++++- .../ListenOptionsHttpsExtensions.cs | 205 +++++++++--- src/Kestrel.Core/Properties/AssemblyInfo.cs | 1 + .../Properties/CoreStrings.Designer.cs | 70 +++++ src/Kestrel/Internal/DefaultHttpsProvider.cs | 42 --- src/Kestrel/Kestrel.csproj | 2 - src/Kestrel/KestrelStrings.resx | 123 -------- .../Properties/KestrelStrings.Designer.cs | 44 --- .../WebHostBuilderKestrelExtensions.cs | 1 - test/Kestrel.Core.Tests/AddressBinderTests.cs | 26 +- .../Kestrel.Core.Tests.csproj | 1 + .../KestrelServerOptionsTests.cs | 35 +++ test/Kestrel.Core.Tests/KestrelServerTests.cs | 38 ++- .../AddressRegistrationTests.cs | 81 ++++- .../CertificateLoaderTests.cs | 64 ++++ test/Kestrel.FunctionalTests/HttpsTests.cs | 65 +++- .../Kestrel.Tests/ConfigurationReaderTests.cs | 177 +++++++++++ test/Kestrel.Tests/Kestrel.Tests.csproj | 6 + .../KestrelConfigurationBuilderTests.cs | 213 +++++++++++++ test/SystemdActivation/docker-entrypoint.sh | 4 +- test/SystemdActivation/docker.sh | 6 +- test/shared/TestResources.cs | 6 + 46 files changed, 1903 insertions(+), 399 deletions(-) create mode 100644 samples/SampleApp/appsettings.Development.json create mode 100644 samples/SampleApp/appsettings.Production.json create mode 100644 samples/SampleApp/appsettings.json create mode 100644 samples/SystemdTestApp/Startup.cs create mode 100644 samples/SystemdTestApp/SystemdTestApp.csproj create mode 100644 samples/SystemdTestApp/testCert.pfx create mode 100644 src/Kestrel.Core/EndpointConfiguration.cs create mode 100644 src/Kestrel.Core/Internal/CertificateLoader.cs create mode 100644 src/Kestrel.Core/Internal/ConfigurationReader.cs delete mode 100644 src/Kestrel.Core/Internal/IDefaultHttpsProvider.cs rename src/{Kestrel => Kestrel.Core}/Internal/LoggerExtensions.cs (94%) create mode 100644 src/Kestrel.Core/KestrelConfigurationLoader.cs delete mode 100644 src/Kestrel/Internal/DefaultHttpsProvider.cs delete mode 100644 src/Kestrel/KestrelStrings.resx delete mode 100644 src/Kestrel/Properties/KestrelStrings.Designer.cs create mode 100644 test/Kestrel.FunctionalTests/CertificateLoaderTests.cs create mode 100644 test/Kestrel.Tests/ConfigurationReaderTests.cs create mode 100644 test/Kestrel.Tests/KestrelConfigurationBuilderTests.cs diff --git a/KestrelHttpServer.sln b/KestrelHttpServer.sln index 7718ccf9e7..345cf04f72 100644 --- a/KestrelHttpServer.sln +++ b/KestrelHttpServer.sln @@ -1,6 +1,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 -VisualStudioVersion = 15.0.27110.0 +VisualStudioVersion = 15.0.27130.2010 MinimumVisualStudioVersion = 15.0.26730.03 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{7972A5D6-3385-4127-9277-428506DD44FF}" ProjectSection(SolutionItems) = preProject @@ -104,6 +104,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Protocols.Abstractions", "s EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{C2910A13-B2C2-46D8-81D8-7E166F4F5981}" ProjectSection(SolutionItems) = preProject + build\dependencies.props = build\dependencies.props build\repo.props = build\repo.props build\repo.targets = build\repo.targets EndProjectSection @@ -119,7 +120,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Kestrel.Transport.Libuv.Fun EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Kestrel.Transport.Sockets.FunctionalTests", "test\Kestrel.Transport.Sockets.FunctionalTests\Kestrel.Transport.Sockets.FunctionalTests.csproj", "{9C7B6B5F-088A-436E-834B-6373EA36DEEE}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Http2SampleApp", "samples\Http2SampleApp\Http2SampleApp.csproj", "{7BC22A4A-15D2-44C2-AB45-049F0FB562FA}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Http2SampleApp", "samples\Http2SampleApp\Http2SampleApp.csproj", "{7BC22A4A-15D2-44C2-AB45-049F0FB562FA}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SystemdTestApp", "samples\SystemdTestApp\SystemdTestApp.csproj", "{A7994A41-CAF8-47A7-8975-F101F75B5BC1}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -335,6 +338,18 @@ Global {7BC22A4A-15D2-44C2-AB45-049F0FB562FA}.Release|x64.Build.0 = Release|Any CPU {7BC22A4A-15D2-44C2-AB45-049F0FB562FA}.Release|x86.ActiveCfg = Release|Any CPU {7BC22A4A-15D2-44C2-AB45-049F0FB562FA}.Release|x86.Build.0 = Release|Any CPU + {A7994A41-CAF8-47A7-8975-F101F75B5BC1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A7994A41-CAF8-47A7-8975-F101F75B5BC1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A7994A41-CAF8-47A7-8975-F101F75B5BC1}.Debug|x64.ActiveCfg = Debug|Any CPU + {A7994A41-CAF8-47A7-8975-F101F75B5BC1}.Debug|x64.Build.0 = Debug|Any CPU + {A7994A41-CAF8-47A7-8975-F101F75B5BC1}.Debug|x86.ActiveCfg = Debug|Any CPU + {A7994A41-CAF8-47A7-8975-F101F75B5BC1}.Debug|x86.Build.0 = Debug|Any CPU + {A7994A41-CAF8-47A7-8975-F101F75B5BC1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A7994A41-CAF8-47A7-8975-F101F75B5BC1}.Release|Any CPU.Build.0 = Release|Any CPU + {A7994A41-CAF8-47A7-8975-F101F75B5BC1}.Release|x64.ActiveCfg = Release|Any CPU + {A7994A41-CAF8-47A7-8975-F101F75B5BC1}.Release|x64.Build.0 = Release|Any CPU + {A7994A41-CAF8-47A7-8975-F101F75B5BC1}.Release|x86.ActiveCfg = Release|Any CPU + {A7994A41-CAF8-47A7-8975-F101F75B5BC1}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -359,6 +374,7 @@ Global {74032D79-8EA7-4483-BD82-C38370420FFF} = {D3273454-EA07-41D2-BF0B-FCC3675C2483} {9C7B6B5F-088A-436E-834B-6373EA36DEEE} = {D3273454-EA07-41D2-BF0B-FCC3675C2483} {7BC22A4A-15D2-44C2-AB45-049F0FB562FA} = {8A3D00B8-1CCF-4BE6-A060-11104CE2D9CE} + {A7994A41-CAF8-47A7-8975-F101F75B5BC1} = {8A3D00B8-1CCF-4BE6-A060-11104CE2D9CE} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {2D10D020-6770-47CA-BB8D-2C23FE3AE071} diff --git a/build/dependencies.props b/build/dependencies.props index 511e04d92d..b6e10ad8cb 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -16,6 +16,9 @@ 2.1.0-preview1-27845 2.1.0-preview1-27845 2.1.0-preview1-27845 + 2.1.0-preview1-27845 + 2.1.0-preview1-27845 + 2.1.0-preview1-27845 2.1.0-preview1-27845 2.1.0-preview1-27845 2.1.0-preview1-27845 diff --git a/samples/SampleApp/SampleApp.csproj b/samples/SampleApp/SampleApp.csproj index a7ba9be9d6..112987f619 100644 --- a/samples/SampleApp/SampleApp.csproj +++ b/samples/SampleApp/SampleApp.csproj @@ -12,11 +12,26 @@ + - + + PreserveNewest + + + + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + diff --git a/samples/SampleApp/Startup.cs b/samples/SampleApp/Startup.cs index e0e471b635..09baead9da 100644 --- a/samples/SampleApp/Startup.cs +++ b/samples/SampleApp/Startup.cs @@ -3,13 +3,15 @@ using System; using System.Diagnostics; -using System.Globalization; using System.IO; using System.Net; +using System.Security.Authentication; +using System.Security.Cryptography.X509Certificates; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; @@ -48,10 +50,31 @@ namespace SampleApp { factory.AddConsole(); }) + .ConfigureAppConfiguration((hostingContext, config) => + { + var env = hostingContext.HostingEnvironment; + config.AddJsonFile("appsettings.json", optional: true) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true); + }) .UseKestrel((context, options) => { + if (context.HostingEnvironment.IsDevelopment()) + { + ShowConfig(context.Configuration); + } + var basePort = context.Configuration.GetValue("BASE_PORT") ?? 5000; + options.ConfigureEndpointDefaults(opt => + { + opt.Protocols = HttpProtocols.Http1; + }); + + options.ConfigureHttpsDefaults(httpsOptions => + { + httpsOptions.SslProtocols = SslProtocols.Tls12; + }); + // Run callbacks on the transport thread options.ApplicationSchedulingMode = SchedulingMode.Inline; @@ -71,11 +94,34 @@ namespace SampleApp options.ListenLocalhost(basePort + 2, listenOptions => { - listenOptions.UseHttps("testCert.pfx", "testPassword"); + // Use default dev cert + listenOptions.UseHttps(); }); options.ListenAnyIP(basePort + 3); + options.ListenAnyIP(basePort + 4, listenOptions => + { + listenOptions.UseHttps(StoreName.My, "aspnet.test", allowInvalid: true); + }); + + options + .Configure() + .Endpoint(IPAddress.Loopback, basePort + 5) + .LocalhostEndpoint(basePort + 6) + .Load(); + + options + .Configure(context.Configuration.GetSection("Kestrel")) + .Endpoint("NamedEndpoint", opt => + { + opt.ListenOptions.Protocols = HttpProtocols.Http1; + }) + .Endpoint("NamedHttpsEndpoint", opt => + { + opt.HttpsOptions.SslProtocols = SslProtocols.Tls12; + }); + options.UseSystemd(); // The following section should be used to demo sockets @@ -96,5 +142,14 @@ namespace SampleApp return hostBuilder.Build().RunAsync(); } + + private static void ShowConfig(IConfiguration config) + { + foreach (var pair in config.GetChildren()) + { + Console.WriteLine($"{pair.Path} - {pair.Value}"); + ShowConfig(pair); + } + } } } \ No newline at end of file diff --git a/samples/SampleApp/appsettings.Development.json b/samples/SampleApp/appsettings.Development.json new file mode 100644 index 0000000000..9edfa91a20 --- /dev/null +++ b/samples/SampleApp/appsettings.Development.json @@ -0,0 +1,15 @@ +{ + "Kestrel": { + "Endpoints": { + "NamedEndpoint": { "Url": "http://localhost:6000" }, + "NamedHttpsEndpoint": { + "Url": "https://localhost:6443", + "Certificate": { + "Subject": "aspnet.test", + "Store": "My", + "AllowInvalid": true + } + } + } + } +} diff --git a/samples/SampleApp/appsettings.Production.json b/samples/SampleApp/appsettings.Production.json new file mode 100644 index 0000000000..8719fb89b7 --- /dev/null +++ b/samples/SampleApp/appsettings.Production.json @@ -0,0 +1,14 @@ +{ + "Kestrel": { + "Endpoints": { + "NamedEndpoint": { "Url": "http://*:6000" }, + "NamedHttpsEndpoint": { + "Url": "https://*:6443", + "Certificate": { + "Path": "testCert.pfx", + "Password": "testPassword" + } + } + } + } +} diff --git a/samples/SampleApp/appsettings.json b/samples/SampleApp/appsettings.json new file mode 100644 index 0000000000..cd77ddd218 --- /dev/null +++ b/samples/SampleApp/appsettings.json @@ -0,0 +1,6 @@ +{ + "Kestrel": { + "Endpoints": { + } + } +} diff --git a/samples/SystemdTestApp/Startup.cs b/samples/SystemdTestApp/Startup.cs new file mode 100644 index 0000000000..0b3c5e05de --- /dev/null +++ b/samples/SystemdTestApp/Startup.cs @@ -0,0 +1,92 @@ +// 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.Diagnostics; +using System.IO; +using System.Net; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; + +namespace SystemdTestApp +{ + public class Startup + { + public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory) + { + var logger = loggerFactory.CreateLogger("Default"); + + app.Run(async context => + { + var connectionFeature = context.Connection; + logger.LogDebug($"Peer: {connectionFeature.RemoteIpAddress?.ToString()}:{connectionFeature.RemotePort}" + + $"{Environment.NewLine}" + + $"Sock: {connectionFeature.LocalIpAddress?.ToString()}:{connectionFeature.LocalPort}"); + + var response = $"hello, world{Environment.NewLine}"; + context.Response.ContentLength = response.Length; + context.Response.ContentType = "text/plain"; + await context.Response.WriteAsync(response); + }); + } + + public static Task Main(string[] args) + { + TaskScheduler.UnobservedTaskException += (sender, e) => + { + Console.WriteLine("Unobserved exception: {0}", e.Exception); + }; + + var hostBuilder = new WebHostBuilder() + .ConfigureLogging((_, factory) => + { + factory.AddConsole(); + }) + .UseKestrel((context, options) => + { + var basePort = context.Configuration.GetValue("BASE_PORT") ?? 5000; + + // Run callbacks on the transport thread + options.ApplicationSchedulingMode = SchedulingMode.Inline; + + options.Listen(IPAddress.Loopback, basePort, listenOptions => + { + // Uncomment the following to enable Nagle's algorithm for this endpoint. + //listenOptions.NoDelay = false; + + listenOptions.UseConnectionLogging(); + }); + + options.Listen(IPAddress.Loopback, basePort + 1, listenOptions => + { + listenOptions.UseHttps("testCert.pfx", "testPassword"); + listenOptions.UseConnectionLogging(); + }); + + options.UseSystemd(); + + // The following section should be used to demo sockets + //options.ListenUnixSocket("/tmp/kestrel-test.sock"); + }) + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseStartup(); + + if (string.Equals(Process.GetCurrentProcess().Id.ToString(), Environment.GetEnvironmentVariable("LISTEN_PID"))) + { + // Use libuv if activated by systemd, since that's currently the only transport that supports being passed a socket handle. + hostBuilder.UseLibuv(options => + { + // Uncomment the following line to change the default number of libuv threads for all endpoints. + // options.ThreadCount = 4; + }); + } + + return hostBuilder.Build().RunAsync(); + } + } +} \ No newline at end of file diff --git a/samples/SystemdTestApp/SystemdTestApp.csproj b/samples/SystemdTestApp/SystemdTestApp.csproj new file mode 100644 index 0000000000..08a5547858 --- /dev/null +++ b/samples/SystemdTestApp/SystemdTestApp.csproj @@ -0,0 +1,23 @@ + + + + netcoreapp2.1;netcoreapp2.0;net461 + false + true + + + + + + + + + + + + + PreserveNewest + + + + diff --git a/samples/SystemdTestApp/testCert.pfx b/samples/SystemdTestApp/testCert.pfx new file mode 100644 index 0000000000000000000000000000000000000000..7118908c2d730670c16e9f8b2c532a262c951989 GIT binary patch literal 2483 zcmaKuc|27A8pqF>IWr86E&Q@(n=B)p$ug!;QVB6xij*z;uPLG!yCz#DQB)+9G$9m9 zQU)=DWXU?*EZIwG!+0d++P@yZ4Xhoagg?p6B~|Ue7tN=Ny=UD?x#1n1MTq z#c9MHh+D#gd|(a(cN}8i91v^=GcdgW3SmA$49p~gM-dys3jVWdg8+!iVL)pz1LDE5 zSb=|GAn(@R=(Ux!MfS9@}sFu-xDd zIt2+mqSq$glwy_6UNs<2?(qERU!gJ;5j}Pp&6trxG=wi)=@k(w2+fJVnc+qvXVzy(>Om4;L|^)R`t*3nTpAmEmTl(#i!RV#a0t#u6>Q9mY`-Nmcs7$XjXT7 zUmCD`O~_j7!%R#I?cG-7C^hcH)@l?WC1vyw$FFu_(r)jhOq6p}W8sG7NO{YTy8tG4 zrb$tTkag*G?(7lfoGx$4YWui>{{@}-FB2ub=}RX{1zx?j)s-##J9|G7E1@-;7Nuln z9MQoX7FJ76+D#XXT@ZZmLZCufIdf3@OigG6m8I7!GT=7VD|>?6e!z9=eT}*E_tSn6 zl+clHCZ-kcIR#gen#LjMJW8>0QtViaQB#FhqsCb0YPYr3;jRITl@V9Aph24D?r2d` zetCyyCg<*O-u+M& zW^ptmT|}p$VAOZpmbQ1{5fK-6ytEvre#Po}6c2URn`viQAF2+e?Z~PK2&pd>7=7)I zTCYm)@3PFRu_6a6Kb)IpCzQ%e3l%O#SDA+$Pq{Dk{HCqi7z>qd{nVpebffL7h{c4( zmhXn~G+C27S3(IfC)q2KON=YwqHXEo%zc40DgWLzF{%RIdr@RcLu90qMSHf!Y}JaqP<={8_Rfe;ddR5= zKEo;^Yip&^m((#{czE{kUga3-@`*;&EwO}Jt>QdURP2P>ob^j-A!qld-0S_pm)kjs zkNo48oZnMt){W~o8g^f;4#?lRLr-T@f}wH1o~-Iq=NEVtTVEZ`vrW~!>2yh%;Bc~H zHl&OK>n@d`*e19*9#v>zZpU?I);f7}IPIfSSk#N|ujE492Itg)l!)TJ19@FE^x|p= zH16NC7OfK&|6_!AnWfTIf^YPOa&`|nbk3VR0vql6&s@y1V3QOU%(`Re+kJgrz?r9!{^wOQ4W-eng23gc}f(LxIs zH_Ls~5izbjcRQH#WH6s6hR;zn>j_R8aJ$A)6xNneu8UI-vWV8Z@HZu&WwvG5q{1ZS zdZeVf{Pv5-u281~y;aJe*x%Uv0@biMZ$vPbKj}O`(SOWQc~kJX` zXR&d4DtAe@2RH$^ z0os5*;0eIUeJi3Uh`A%44x(XzjClG8BO~-r_A}odiRuHo2-86#`mhrgN5p~<$RLY? zq(kynfFA5{v#p+EA1 z5aoe1763EQHorRm`C&ktKn(OQ1n)$Q{GZz&jRb`eDEMpl<0O#+)DMV(T7nsIzCG{QuM->B9g7Lrl2SE&gW`M!~(un|y0fIn=b^6_$ z9{zEzgYI~39xn0ZP*9qBL%fg7rg$ttt&TOmvfNNO<6FT0ZavM$Y4CYLQGIcIYv9Y& zBGPUh&QTfW;V2!)oIra@s&d968y-y}Y|ww(R$GzWS*V&)k@W0>Slem{|HdTCjm;_5 zwY*A8W3nUbemE^_f0ng$tbd<`sr?TO-_&VCw+F#7P@LkIl$1PzTBoPY1b88EIO>UO zP-NK7+g2yD3U6g3i|iA6+su>54sf_Sk0F=)1|9odnCM4u2Rs z=&Y?-V&VquSN%3FJ2~ZGweP~iLs|w=l@9yu$tj@}Dp?e-2JUsqOoswdXb=E%&0te_ zA2M+{5Hf-dqD7=yw*r@A*xkn(1IS~nfP}k}e?4Bt|9g(eph4hFX_|S6nj1&Sz9z^= zRw~<&-9d@FzTn6S*RVE{Wj5lgLJr9HLB8S9CgOm*>XA8*y4`JE;^s$=bqD#U4;e5C&x&ggKIAVL zrQ)Yd8|{>7Z(6*B&7&4&9(*vDOfHMuR-Dk1IZia*XM^EZUD^{?cWG>J>KrtElc*{K zaVl(7SN2cH4I6Q$bZOpJ8e5LKaG7p;?tJ~#+9QrTYU@f#5`Vo7cEX!szCT}iX-K^2 w#3o+=C+lQz2J+SOEzVX(eJ)e7=eicC{rr9U2VGDcdH?_b literal 0 HcmV?d00001 diff --git a/src/Kestrel.Core/CoreStrings.resx b/src/Kestrel.Core/CoreStrings.resx index 49ee7e20b8..2babfee801 100644 --- a/src/Kestrel.Core/CoreStrings.resx +++ b/src/Kestrel.Core/CoreStrings.resx @@ -480,4 +480,19 @@ The server certificate parameter is required. + + No listening endpoints were configured. Binding to {address0} and {address1} by default. + + + The requested certificate {subject} could not be found in {storeLocation}/{storeName} with AllowInvalid setting: {allowInvalid}. + + + The endpoint {endpointName} is missing the required 'Url' parameter. + + + Unable to configure HTTPS endpoint. No server certificate was specified and the default developer certificate could not be found. Try running 'dotnet developercertificates https -t' to setup a developer certificate for use with localhost. For information on configuring HTTPS see https://go.microsoft.com/fwlink/?linkid=848054 + + + The endpoint {endpointName} specified multiple certificate sources. + \ No newline at end of file diff --git a/src/Kestrel.Core/EndpointConfiguration.cs b/src/Kestrel.Core/EndpointConfiguration.cs new file mode 100644 index 0000000000..94848b14bd --- /dev/null +++ b/src/Kestrel.Core/EndpointConfiguration.cs @@ -0,0 +1,26 @@ +// 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.Server.Kestrel.Core; +using Microsoft.AspNetCore.Server.Kestrel.Https; +using Microsoft.Extensions.Configuration; + +namespace Microsoft.AspNetCore.Server.Kestrel +{ + public class EndpointConfiguration + { + internal EndpointConfiguration(bool isHttps, ListenOptions listenOptions, HttpsConnectionAdapterOptions httpsOptions, IConfigurationSection configSection) + { + IsHttps = isHttps; + ListenOptions = listenOptions ?? throw new ArgumentNullException(nameof(listenOptions)); + HttpsOptions = httpsOptions ?? throw new ArgumentNullException(nameof(httpsOptions)); + ConfigSection = configSection ?? throw new ArgumentNullException(nameof(configSection)); + } + + public bool IsHttps { get; } + public ListenOptions ListenOptions { get; } + public HttpsConnectionAdapterOptions HttpsOptions { get; } + public IConfigurationSection ConfigSection { get; } + } +} diff --git a/src/Kestrel.Core/Internal/AddressBindContext.cs b/src/Kestrel.Core/Internal/AddressBindContext.cs index e2f46e6025..f4c1859b7f 100644 --- a/src/Kestrel.Core/Internal/AddressBindContext.cs +++ b/src/Kestrel.Core/Internal/AddressBindContext.cs @@ -14,7 +14,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal public List ListenOptions { get; set; } public KestrelServerOptions ServerOptions { get; set; } public ILogger Logger { get; set; } - public IDefaultHttpsProvider DefaultHttpsProvider { get; set; } public Func CreateBinding { get; set; } } diff --git a/src/Kestrel.Core/Internal/AddressBinder.cs b/src/Kestrel.Core/Internal/AddressBinder.cs index 5334308ad8..96ec14e51b 100644 --- a/src/Kestrel.Core/Internal/AddressBinder.cs +++ b/src/Kestrel.Core/Internal/AddressBinder.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Net; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting.Server.Features; using Microsoft.AspNetCore.Protocols; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; @@ -20,7 +21,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal public static async Task BindAsync(IServerAddressesFeature addresses, KestrelServerOptions serverOptions, ILogger logger, - IDefaultHttpsProvider defaultHttpsProvider, Func createBinding) { var listenOptions = serverOptions.ListenOptions; @@ -35,7 +35,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal ListenOptions = listenOptions, ServerOptions = serverOptions, Logger = logger, - DefaultHttpsProvider = defaultHttpsProvider ?? UnconfiguredDefaultHttpsProvider.Instance, CreateBinding = createBinding }; @@ -112,10 +111,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal context.ListenOptions.Add(endpoint); } - internal static ListenOptions ParseAddress(string address, KestrelServerOptions serverOptions, IDefaultHttpsProvider defaultHttpsProvider) + internal static ListenOptions ParseAddress(string address, out bool https) { var parsedAddress = ServerAddress.FromUrl(address); - var https = false; + https = false; if (parsedAddress.Scheme.Equals("https", StringComparison.OrdinalIgnoreCase)) { @@ -151,12 +150,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal options = new AnyIPListenOptions(parsedAddress.Port); } - if (https) - { - options.KestrelServerOptions = serverOptions; - defaultHttpsProvider.ConfigureHttps(options); - } - return options; } @@ -169,10 +162,26 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal { public async Task BindAsync(AddressBindContext context) { - context.Logger.LogDebug(CoreStrings.BindingToDefaultAddress, Constants.DefaultServerAddress); + var httpDefault = ParseAddress(Constants.DefaultServerAddress, out var https); + context.ServerOptions.ApplyEndpointDefaults(httpDefault); + await httpDefault.BindAsync(context).ConfigureAwait(false); - await ParseAddress(Constants.DefaultServerAddress, context.ServerOptions, context.DefaultHttpsProvider) - .BindAsync(context).ConfigureAwait(false); + // Conditional https default, only if a cert is available + var httpsDefault = ParseAddress(Constants.DefaultServerHttpsAddress, out https); + context.ServerOptions.ApplyEndpointDefaults(httpsDefault); + + if (httpsDefault.ConnectionAdapters.Any(f => f.IsHttps) + || httpsDefault.TryUseHttps()) + { + await httpsDefault.BindAsync(context).ConfigureAwait(false); + context.Logger.LogDebug(CoreStrings.BindingToDefaultAddresses, + Constants.DefaultServerAddress, Constants.DefaultServerHttpsAddress); + } + else + { + // No default cert is available, do not bind to the https endpoint. + context.Logger.LogDebug(CoreStrings.BindingToDefaultAddress, Constants.DefaultServerAddress); + } } } @@ -242,27 +251,17 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal { foreach (var address in _addresses) { - await ParseAddress(address, context.ServerOptions, context.DefaultHttpsProvider) - .BindAsync(context).ConfigureAwait(false); + var options = ParseAddress(address, out var https); + context.ServerOptions.ApplyEndpointDefaults(options); + + if (https && !options.ConnectionAdapters.Any(f => f.IsHttps)) + { + options.UseHttps(); + } + + await options.BindAsync(context).ConfigureAwait(false); } } } - - private class UnconfiguredDefaultHttpsProvider : IDefaultHttpsProvider - { - public static readonly UnconfiguredDefaultHttpsProvider Instance = new UnconfiguredDefaultHttpsProvider(); - - private UnconfiguredDefaultHttpsProvider() - { - } - - public void ConfigureHttps(ListenOptions listenOptions) - { - // We have to throw here. If this is called, it's because the user asked for "https" binding but for some - // reason didn't provide a certificate and didn't use the "DefaultHttpsProvider". This means if we no-op, - // we'll silently downgrade to HTTP, which is bad. - throw new InvalidOperationException(CoreStrings.UnableToConfigureHttpsBindings); - } - } } } diff --git a/src/Kestrel.Core/Internal/CertificateLoader.cs b/src/Kestrel.Core/Internal/CertificateLoader.cs new file mode 100644 index 0000000000..087fad7d87 --- /dev/null +++ b/src/Kestrel.Core/Internal/CertificateLoader.cs @@ -0,0 +1,99 @@ +// 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 System.Text; +using Microsoft.AspNetCore.Server.Kestrel.Core; + +namespace Microsoft.AspNetCore.Server.Kestrel.Https.Internal +{ + public static class CertificateLoader + { + // See http://oid-info.com/get/1.3.6.1.5.5.7.3.1 + // Indicates that a certificate can be used as a SSL server certificate + private const string ServerAuthenticationOid = "1.3.6.1.5.5.7.3.1"; + + public static X509Certificate2 LoadFromStoreCert(string subject, string storeName, StoreLocation storeLocation, bool allowInvalid) + { + using (var store = new X509Store(storeName, storeLocation)) + { + X509Certificate2Collection storeCertificates = null; + X509Certificate2 foundCertificate = null; + + try + { + store.Open(OpenFlags.ReadOnly); + storeCertificates = store.Certificates; + var foundCertificates = storeCertificates.Find(X509FindType.FindBySubjectName, subject, !allowInvalid); + foundCertificate = foundCertificates + .OfType() + .Where(IsCertificateAllowedForServerAuth) + .OrderByDescending(certificate => certificate.NotAfter) + .FirstOrDefault(); + + if (foundCertificate == null) + { + throw new InvalidOperationException(CoreStrings.FormatCertNotFoundInStore(subject, storeLocation, storeName, allowInvalid)); + } + + return foundCertificate; + } + finally + { + DisposeCertificates(storeCertificates, except: foundCertificate); + } + } + } + + internal static bool IsCertificateAllowedForServerAuth(X509Certificate2 certificate) + { + /* If the Extended Key Usage extension is included, then we check that the serverAuth usage is included. (http://oid-info.com/get/1.3.6.1.5.5.7.3.1) + * If the Extended Key Usage extension is not included, then we assume the certificate is allowed for all usages. + * + * See also https://blogs.msdn.microsoft.com/kaushal/2012/02/17/client-certificates-vs-server-certificates/ + * + * From https://tools.ietf.org/html/rfc3280#section-4.2.1.13 "Certificate Extensions: Extended Key Usage" + * + * If the (Extended Key Usage) extension is present, then the certificate MUST only be used + * for one of the purposes indicated. If multiple purposes are + * indicated the application need not recognize all purposes indicated, + * as long as the intended purpose is present. Certificate using + * applications MAY require that a particular purpose be indicated in + * order for the certificate to be acceptable to that application. + */ + + var hasEkuExtension = false; + + foreach (var extension in certificate.Extensions.OfType()) + { + hasEkuExtension = true; + foreach (var oid in extension.EnhancedKeyUsages) + { + if (oid.Value.Equals(ServerAuthenticationOid, StringComparison.Ordinal)) + { + return true; + } + } + } + + return !hasEkuExtension; + } + + private static void DisposeCertificates(X509Certificate2Collection certificates, X509Certificate2 except) + { + if (certificates != null) + { + foreach (var certificate in certificates) + { + if (!certificate.Equals(except)) + { + certificate.Dispose(); + } + } + } + } + } +} diff --git a/src/Kestrel.Core/Internal/ConfigurationReader.cs b/src/Kestrel.Core/Internal/ConfigurationReader.cs new file mode 100644 index 0000000000..08f347b922 --- /dev/null +++ b/src/Kestrel.Core/Internal/ConfigurationReader.cs @@ -0,0 +1,140 @@ +// 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 Microsoft.Extensions.Configuration; + +namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal +{ + internal class ConfigurationReader + { + private IConfiguration _configuration; + private IDictionary _certificates; + private IList _endpoints; + + public ConfigurationReader(IConfiguration configuration) + { + _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); + } + + public IDictionary Certificates + { + get + { + if (_certificates == null) + { + ReadCertificates(); + } + + return _certificates; + } + } + + public IEnumerable Endpoints + { + get + { + if (_endpoints == null) + { + ReadEndpoints(); + } + + return _endpoints; + } + } + + private void ReadCertificates() + { + _certificates = new Dictionary(0); + + var certificatesConfig = _configuration.GetSection("Certificates").GetChildren(); + foreach (var certificateConfig in certificatesConfig) + { + _certificates.Add(certificateConfig.Key, new CertificateConfig(certificateConfig)); + } + } + + private void ReadEndpoints() + { + _endpoints = new List(); + + var endpointsConfig = _configuration.GetSection("Endpoints").GetChildren(); + foreach (var endpointConfig in endpointsConfig) + { + // "EndpointName": { +        // "Url": "https://*:5463", +        // "Certificate": { +          // "Path": "testCert.pfx", +          // "Password": "testPassword" +       // } + // } + + var url = endpointConfig["Url"]; + if (string.IsNullOrEmpty(url)) + { + throw new InvalidOperationException(CoreStrings.FormatEndpointMissingUrl(endpointConfig.Key)); + } + + var endpoint = new EndpointConfig() + { + Name = endpointConfig.Key, + Url = url, + ConfigSection = endpointConfig, + Certificate = new CertificateConfig(endpointConfig.GetSection("Certificate")), + }; + _endpoints.Add(endpoint); + } + } + } + + // "EndpointName": { + // "Url": "https://*:5463", + // "Certificate": { + // "Path": "testCert.pfx", + // "Password": "testPassword" + // } + // } + internal class EndpointConfig + { + public string Name { get; set; } + public string Url { get; set; } + public IConfigurationSection ConfigSection { get; set; } + public CertificateConfig Certificate { get; set; } + } + + // "CertificateName": { + // "Path": "testCert.pfx", + // "Password": "testPassword" + // } + internal class CertificateConfig + { + public CertificateConfig(IConfigurationSection configSection) + { + ConfigSection = configSection; + ConfigSection.Bind(this); + } + + public IConfigurationSection ConfigSection { get; } + + // File + public bool IsFileCert => !string.IsNullOrEmpty(Path); + + public string Path { get; set; } + + public string Password { get; set; } + + // Cert store + + public bool IsStoreCert => !string.IsNullOrEmpty(Subject); + + public string Subject { get; set; } + + public string Store { get; set; } + + public string Location { get; set; } + + public bool? AllowInvalid { get; set; } + } +} diff --git a/src/Kestrel.Core/Internal/HttpsConnectionAdapter.cs b/src/Kestrel.Core/Internal/HttpsConnectionAdapter.cs index 68043a0762..cae7d60b2f 100644 --- a/src/Kestrel.Core/Internal/HttpsConnectionAdapter.cs +++ b/src/Kestrel.Core/Internal/HttpsConnectionAdapter.cs @@ -19,10 +19,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Https.Internal { public class HttpsConnectionAdapter : IConnectionAdapter { - // See http://oid-info.com/get/1.3.6.1.5.5.7.3.1 - // Indicates that a certificate can be used as a SSL server certificate - private const string ServerAuthenticationOid = "1.3.6.1.5.5.7.3.1"; - private static readonly ClosedAdaptedConnection _closedAdaptedConnection = new ClosedAdaptedConnection(); private readonly HttpsConnectionAdapterOptions _options; @@ -187,36 +183,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Https.Internal private static void EnsureCertificateIsAllowedForServerAuth(X509Certificate2 certificate) { - /* If the Extended Key Usage extension is included, then we check that the serverAuth usage is included. (http://oid-info.com/get/1.3.6.1.5.5.7.3.1) - * If the Extended Key Usage extension is not included, then we assume the certificate is allowed for all usages. - * - * See also https://blogs.msdn.microsoft.com/kaushal/2012/02/17/client-certificates-vs-server-certificates/ - * - * From https://tools.ietf.org/html/rfc3280#section-4.2.1.13 "Certificate Extensions: Extended Key Usage" - * - * If the (Extended Key Usage) extension is present, then the certificate MUST only be used - * for one of the purposes indicated. If multiple purposes are - * indicated the application need not recognize all purposes indicated, - * as long as the intended purpose is present. Certificate using - * applications MAY require that a particular purpose be indicated in - * order for the certificate to be acceptable to that application. - */ - - var hasEkuExtension = false; - - foreach (var extension in certificate.Extensions.OfType()) - { - hasEkuExtension = true; - foreach (var oid in extension.EnhancedKeyUsages) - { - if (oid.Value.Equals(ServerAuthenticationOid, StringComparison.Ordinal)) - { - return; - } - } - } - - if (hasEkuExtension) + if (!CertificateLoader.IsCertificateAllowedForServerAuth(certificate)) { throw new InvalidOperationException(CoreStrings.FormatInvalidServerCertificateEku(certificate.Thumbprint)); } diff --git a/src/Kestrel.Core/Internal/IDefaultHttpsProvider.cs b/src/Kestrel.Core/Internal/IDefaultHttpsProvider.cs deleted file mode 100644 index 3ed67b0cda..0000000000 --- a/src/Kestrel.Core/Internal/IDefaultHttpsProvider.cs +++ /dev/null @@ -1,10 +0,0 @@ -// 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. - -namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal -{ - public interface IDefaultHttpsProvider - { - void ConfigureHttps(ListenOptions listenOptions); - } -} diff --git a/src/Kestrel.Core/Internal/Infrastructure/Constants.cs b/src/Kestrel.Core/Internal/Infrastructure/Constants.cs index 6db5d384ee..8aead7103c 100644 --- a/src/Kestrel.Core/Internal/Infrastructure/Constants.cs +++ b/src/Kestrel.Core/Internal/Infrastructure/Constants.cs @@ -10,10 +10,15 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure public const int MaxExceptionDetailSize = 128; /// - /// The IPEndPoint Kestrel will bind to if nothing else is specified. + /// The endpoint Kestrel will bind to if nothing else is specified. /// public static readonly string DefaultServerAddress = "http://localhost:5000"; + /// + /// The endpoint Kestrel will bind to if nothing else is specified and a default certificate is available. + /// + public static readonly string DefaultServerHttpsAddress = "https://localhost:5001"; + /// /// Prefix of host name used to specify Unix sockets in the configuration. /// diff --git a/src/Kestrel.Core/Internal/KestrelServerOptionsSetup.cs b/src/Kestrel.Core/Internal/KestrelServerOptionsSetup.cs index 4d7a95ca5b..18831f0b49 100644 --- a/src/Kestrel.Core/Internal/KestrelServerOptionsSetup.cs +++ b/src/Kestrel.Core/Internal/KestrelServerOptionsSetup.cs @@ -2,8 +2,12 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using Microsoft.AspNetCore.Hosting.Server.Features; +using System.Linq; +using System.Security.Cryptography.X509Certificates; +using Microsoft.AspNetCore.Certificates.Generation; +using Microsoft.AspNetCore.Server.Kestrel.Internal; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal @@ -20,6 +24,24 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal public void Configure(KestrelServerOptions options) { options.ApplicationServices = _services; + UseDefaultDeveloperCertificate(options); + } + + private void UseDefaultDeveloperCertificate(KestrelServerOptions options) + { + var certificateManager = new CertificateManager(); + var certificate = certificateManager.ListCertificates(CertificatePurpose.HTTPS, StoreName.My, StoreLocation.CurrentUser, isValid: true) + .FirstOrDefault(); + var logger = options.ApplicationServices.GetRequiredService>(); + if (certificate != null) + { + logger.LocatedDevelopmentCertificate(certificate); + options.DefaultCertificate = certificate; + } + else + { + logger.UnableToLocateDevelopmentCertificate(); + } } } } diff --git a/src/Kestrel/Internal/LoggerExtensions.cs b/src/Kestrel.Core/Internal/LoggerExtensions.cs similarity index 94% rename from src/Kestrel/Internal/LoggerExtensions.cs rename to src/Kestrel.Core/Internal/LoggerExtensions.cs index 218f50ca10..1f3b8d131d 100644 --- a/src/Kestrel/Internal/LoggerExtensions.cs +++ b/src/Kestrel.Core/Internal/LoggerExtensions.cs @@ -11,7 +11,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Internal LoggerMessage.Define(LogLevel.Debug, new EventId(0, nameof(LocatedDevelopmentCertificate)), "Using development certificate: {certificateSubjectName} (Thumbprint: {certificateThumbprint})"); private static readonly Action _unableToLocateDevelopmentCertificate = - LoggerMessage.Define(LogLevel.Error, new EventId(1, nameof(UnableToLocateDevelopmentCertificate)), "Unable to locate an appropriate development https certificate."); + LoggerMessage.Define(LogLevel.Debug, new EventId(1, nameof(UnableToLocateDevelopmentCertificate)), "Unable to locate an appropriate development https certificate."); public static void LocatedDevelopmentCertificate(this ILogger logger, X509Certificate2 certificate) => _locatedDevelopmentCertificate(logger, certificate.Subject, certificate.Thumbprint, null); diff --git a/src/Kestrel.Core/Kestrel.Core.csproj b/src/Kestrel.Core/Kestrel.Core.csproj index 75793650b5..2f6ea2a3bb 100644 --- a/src/Kestrel.Core/Kestrel.Core.csproj +++ b/src/Kestrel.Core/Kestrel.Core.csproj @@ -12,14 +12,17 @@ + + + diff --git a/src/Kestrel.Core/KestrelConfigurationLoader.cs b/src/Kestrel.Core/KestrelConfigurationLoader.cs new file mode 100644 index 0000000000..1dfb9d262c --- /dev/null +++ b/src/Kestrel.Core/KestrelConfigurationLoader.cs @@ -0,0 +1,297 @@ +// 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.IO; +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.Core.Internal; +using Microsoft.AspNetCore.Server.Kestrel.Https; +using Microsoft.AspNetCore.Server.Kestrel.Https.Internal; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Server.Kestrel +{ + public class KestrelConfigurationLoader + { + internal KestrelConfigurationLoader(KestrelServerOptions options, IConfiguration configuration) + { + Options = options ?? throw new ArgumentNullException(nameof(options)); + Configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); + } + + public KestrelServerOptions Options { get; } + public IConfiguration Configuration { 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. + private IList EndpointsToAdd { get; } = new List(); + + /// + /// Specifies a configuration Action to run when an endpoint with the given name is loaded from configuration. + /// + public KestrelConfigurationLoader Endpoint(string name, Action configureOptions) + { + if (string.IsNullOrEmpty(name)) + { + throw new ArgumentNullException(nameof(name)); + } + + EndpointConfigurations[name] = configureOptions ?? throw new ArgumentNullException(nameof(configureOptions)); + return this; + } + + /// + /// Bind to given IP address and port. + /// + public KestrelConfigurationLoader Endpoint(IPAddress address, int port) => Endpoint(address, port, _ => { }); + + /// + /// Bind to given IP address and port. + /// + public KestrelConfigurationLoader Endpoint(IPAddress address, int port, Action configure) + { + if (address == null) + { + throw new ArgumentNullException(nameof(address)); + } + + return Endpoint(new IPEndPoint(address, port), configure); + } + + /// + /// Bind to given IP endpoint. + /// + public KestrelConfigurationLoader Endpoint(IPEndPoint endPoint) => Endpoint(endPoint, _ => { }); + + /// + /// Bind to given IP address and port. + /// + public KestrelConfigurationLoader Endpoint(IPEndPoint endPoint, Action configure) + { + if (endPoint == null) + { + throw new ArgumentNullException(nameof(endPoint)); + } + if (configure == null) + { + throw new ArgumentNullException(nameof(configure)); + } + + EndpointsToAdd.Add(() => + { + Options.Listen(endPoint, configure); + }); + + return this; + } + + /// + /// Listens on ::1 and 127.0.0.1 with the given port. Requesting a dynamic port by specifying 0 is not supported + /// for this type of endpoint. + /// + public KestrelConfigurationLoader LocalhostEndpoint(int port) => LocalhostEndpoint(port, options => { }); + + /// + /// Listens on ::1 and 127.0.0.1 with the given port. Requesting a dynamic port by specifying 0 is not supported + /// for this type of endpoint. + /// + public KestrelConfigurationLoader LocalhostEndpoint(int port, Action configure) + { + if (configure == null) + { + throw new ArgumentNullException(nameof(configure)); + } + + EndpointsToAdd.Add(() => + { + Options.ListenLocalhost(port, configure); + }); + + return this; + } + + /// + /// Listens on all IPs using IPv6 [::], or IPv4 0.0.0.0 if IPv6 is not supported. + /// + public KestrelConfigurationLoader AnyIPEndpoint(int port) => AnyIPEndpoint(port, options => { }); + + /// + /// Listens on all IPs using IPv6 [::], or IPv4 0.0.0.0 if IPv6 is not supported. + /// + public KestrelConfigurationLoader AnyIPEndpoint(int port, Action configure) + { + if (configure == null) + { + throw new ArgumentNullException(nameof(configure)); + } + + EndpointsToAdd.Add(() => + { + Options.ListenAnyIP(port, configure); + }); + + return this; + } + + /// + /// Bind to given Unix domain socket path. + /// + public KestrelConfigurationLoader UnixSocketEndpoint(string socketPath) => UnixSocketEndpoint(socketPath, _ => { }); + + /// + /// Bind to given Unix domain socket path. + /// + public KestrelConfigurationLoader UnixSocketEndpoint(string socketPath, Action configure) + { + if (socketPath == null) + { + throw new ArgumentNullException(nameof(socketPath)); + } + if (socketPath.Length == 0 || socketPath[0] != '/') + { + throw new ArgumentException(CoreStrings.UnixSocketPathMustBeAbsolute, nameof(socketPath)); + } + if (configure == null) + { + throw new ArgumentNullException(nameof(configure)); + } + + EndpointsToAdd.Add(() => + { + Options.ListenUnixSocket(socketPath, configure); + }); + + return this; + } + + /// + /// Open a socket file descriptor. + /// + public KestrelConfigurationLoader HandleEndpoint(ulong handle) => HandleEndpoint(handle, _ => { }); + + /// + /// Open a socket file descriptor. + /// + public KestrelConfigurationLoader HandleEndpoint(ulong handle, Action configure) + { + if (configure == null) + { + throw new ArgumentNullException(nameof(configure)); + } + + EndpointsToAdd.Add(() => + { + Options.ListenHandle(handle, configure); + }); + + return this; + } + + public void Load() + { + if (Options.ConfigurationLoader == null) + { + // The loader has already been run. + return; + } + Options.ConfigurationLoader = null; + + var configReader = new ConfigurationReader(Configuration); + + LoadDefaultCert(configReader); + + foreach (var endpoint in configReader.Endpoints) + { + var listenOptions = AddressBinder.ParseAddress(endpoint.Url, out var https); + Options.ApplyEndpointDefaults(listenOptions); + + // Compare to UseHttps(httpsOptions => { }) + var httpsOptions = new HttpsConnectionAdapterOptions(); + if (https) + { + // Defaults + Options.ApplyHttpsDefaults(httpsOptions); + + // Specified + httpsOptions.ServerCertificate = LoadCertificate(endpoint.Certificate, endpoint.Name) + ?? httpsOptions.ServerCertificate; + } + + if (EndpointConfigurations.TryGetValue(endpoint.Name, out var configureEndpoint)) + { + var endpointConfig = new EndpointConfiguration(https, listenOptions, httpsOptions, endpoint.ConfigSection); + configureEndpoint(endpointConfig); + } + + // EndpointDefaults or configureEndpoint may have added an https adapter. + if (https && !listenOptions.ConnectionAdapters.Any(f => f.IsHttps)) + { + if (httpsOptions.ServerCertificate == null) + { + throw new InvalidOperationException(CoreStrings.NoCertSpecifiedNoDevelopmentCertificateFound); + } + + listenOptions.UseHttps(httpsOptions); + } + + Options.ListenOptions.Add(listenOptions); + } + + foreach (var action in EndpointsToAdd) + { + action(); + } + } + + private void LoadDefaultCert(ConfigurationReader configReader) + { + if (configReader.Certificates.TryGetValue("Default", out var defaultCertConfig)) + { + var defaultCert = LoadCertificate(defaultCertConfig, "Default"); + if (defaultCert != null) + { + Options.DefaultCertificate = defaultCert; + } + } + } + + private X509Certificate2 LoadCertificate(CertificateConfig certInfo, string endpointName) + { + if (certInfo.IsFileCert && certInfo.IsStoreCert) + { + throw new InvalidOperationException(CoreStrings.FormatMultipleCertificateSources(endpointName)); + } + else if (certInfo.IsFileCert) + { + var env = Options.ApplicationServices.GetRequiredService(); + return new X509Certificate2(Path.Combine(env.ContentRootPath, certInfo.Path), certInfo.Password); + } + else if (certInfo.IsStoreCert) + { + return LoadFromStoreCert(certInfo); + } + return null; + } + + private static X509Certificate2 LoadFromStoreCert(CertificateConfig certInfo) + { + var subject = certInfo.Subject; + var storeName = certInfo.Store; + var location = certInfo.Location; + var storeLocation = StoreLocation.CurrentUser; + if (!string.IsNullOrEmpty(location)) + { + storeLocation = (StoreLocation)Enum.Parse(typeof(StoreLocation), location, ignoreCase: true); + } + var allowInvalid = certInfo.AllowInvalid ?? false; + + return CertificateLoader.LoadFromStoreCert(subject, storeName, storeLocation, allowInvalid); + } + } +} diff --git a/src/Kestrel.Core/KestrelServer.cs b/src/Kestrel.Core/KestrelServer.cs index 1905ab7bc6..43b7ee3981 100644 --- a/src/Kestrel.Core/KestrelServer.cs +++ b/src/Kestrel.Core/KestrelServer.cs @@ -22,7 +22,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core private readonly List _transports = new List(); private readonly Heartbeat _heartbeat; private readonly IServerAddressesFeature _serverAddresses; - private readonly IDefaultHttpsProvider _defaultHttpsProvider; private readonly ITransportFactory _transportFactory; private bool _hasStarted; @@ -34,12 +33,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core { } - public KestrelServer(IOptions options, ITransportFactory transportFactory, ILoggerFactory loggerFactory, IDefaultHttpsProvider defaultHttpsProvider) - : this(transportFactory, CreateServiceContext(options, loggerFactory)) - { - _defaultHttpsProvider = defaultHttpsProvider; - } - // For testing internal KestrelServer(ITransportFactory transportFactory, ServiceContext serviceContext) { @@ -159,7 +152,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core await transport.BindAsync().ConfigureAwait(false); } - await AddressBinder.BindAsync(_serverAddresses, Options, Trace, _defaultHttpsProvider, OnBind).ConfigureAwait(false); + await AddressBinder.BindAsync(_serverAddresses, Options, Trace, OnBind).ConfigureAwait(false); } catch (Exception ex) { @@ -224,6 +217,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core private void ValidateOptions() { + Options.ConfigurationLoader?.Load(); + if (Options.Limits.MaxRequestBufferSize.HasValue && Options.Limits.MaxRequestBufferSize < Options.Limits.MaxRequestLineSize) { diff --git a/src/Kestrel.Core/KestrelServerOptions.cs b/src/Kestrel.Core/KestrelServerOptions.cs index 3d70664cbe..b4354d4c0a 100644 --- a/src/Kestrel.Core/KestrelServerOptions.cs +++ b/src/Kestrel.Core/KestrelServerOptions.cs @@ -4,8 +4,11 @@ using System; using System.Collections.Generic; using System.Net; +using System.Security.Cryptography.X509Certificates; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Server.Kestrel.Https; using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal; +using Microsoft.Extensions.Configuration; namespace Microsoft.AspNetCore.Server.Kestrel.Core { @@ -55,6 +58,78 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core /// public KestrelServerLimits Limits { get; } = new KestrelServerLimits(); + /// + /// Provides a configuration source where endpoints will be loaded from on server start. + /// The default is null. + /// + public KestrelConfigurationLoader ConfigurationLoader { get; set; } + + /// + /// A default configuration action for all endpoints. Use for Listen, configuration, the default url, and URLs. + /// + private Action EndpointDefaults { get; set; } = _ => { }; + + /// + /// A default configuration action for all https endpoints. + /// + private Action HttpsDefaults { get; set; } = _ => { }; + + /// + /// The default server certificate for https endpoints. This is applied before HttpsDefaults. + /// + internal X509Certificate2 DefaultCertificate { get; set; } + + /// + /// Specifies a configuration Action to run for each newly created endpoint. Calling this again will replace + /// the prior action. + /// + public void ConfigureEndpointDefaults(Action configureOptions) + { + EndpointDefaults = configureOptions ?? throw new ArgumentNullException(nameof(configureOptions)); + } + + internal void ApplyEndpointDefaults(ListenOptions listenOptions) + { + listenOptions.KestrelServerOptions = this; + EndpointDefaults(listenOptions); + } + + /// + /// Specifies a configuration Action to run for each newly created https endpoint. Calling this again will replace + /// the prior action. + /// + public void ConfigureHttpsDefaults(Action configureOptions) + { + HttpsDefaults = configureOptions ?? throw new ArgumentNullException(nameof(configureOptions)); + } + + internal void ApplyHttpsDefaults(HttpsConnectionAdapterOptions httpsOptions) + { + httpsOptions.ServerCertificate = DefaultCertificate; + HttpsDefaults(httpsOptions); + } + + /// + /// Creates a configuration loader for setting up Kestrel. + /// + public KestrelConfigurationLoader Configure() + { + var loader = new KestrelConfigurationLoader(this, new ConfigurationBuilder().Build()); + ConfigurationLoader = loader; + return loader; + } + + /// + /// Creates a configuration loader for setting up Kestrel that takes an IConfiguration as input. + /// This configuration must be scoped to the configuration section for Kestrel. + /// + public KestrelConfigurationLoader Configure(IConfiguration config) + { + var loader = new KestrelConfigurationLoader(this, config); + ConfigurationLoader = loader; + return loader; + } + /// /// Bind to given IP address and port. /// @@ -100,13 +175,22 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core throw new ArgumentNullException(nameof(configure)); } - var listenOptions = new ListenOptions(endPoint) { KestrelServerOptions = this }; + var listenOptions = new ListenOptions(endPoint); + ApplyEndpointDefaults(listenOptions); configure(listenOptions); ListenOptions.Add(listenOptions); } + /// + /// Listens on ::1 and 127.0.0.1 with the given port. Requesting a dynamic port by specifying 0 is not supported + /// for this type of endpoint. + /// public void ListenLocalhost(int port) => ListenLocalhost(port, options => { }); + /// + /// Listens on ::1 and 127.0.0.1 with the given port. Requesting a dynamic port by specifying 0 is not supported + /// for this type of endpoint. + /// public void ListenLocalhost(int port, Action configure) { if (configure == null) @@ -114,16 +198,20 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core throw new ArgumentNullException(nameof(configure)); } - var listenOptions = new LocalhostListenOptions(port) - { - KestrelServerOptions = this, - }; + var listenOptions = new LocalhostListenOptions(port); + ApplyEndpointDefaults(listenOptions); configure(listenOptions); ListenOptions.Add(listenOptions); } + /// + /// Listens on all IPs using IPv6 [::], or IPv4 0.0.0.0 if IPv6 is not supported. + /// public void ListenAnyIP(int port) => ListenAnyIP(port, options => { }); + /// + /// Listens on all IPs using IPv6 [::], or IPv4 0.0.0.0 if IPv6 is not supported. + /// public void ListenAnyIP(int port, Action configure) { if (configure == null) @@ -131,10 +219,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core throw new ArgumentNullException(nameof(configure)); } - var listenOptions = new AnyIPListenOptions(port) - { - KestrelServerOptions = this, - }; + var listenOptions = new AnyIPListenOptions(port); + ApplyEndpointDefaults(listenOptions); configure(listenOptions); ListenOptions.Add(listenOptions); } @@ -166,7 +252,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core throw new ArgumentNullException(nameof(configure)); } - var listenOptions = new ListenOptions(socketPath) { KestrelServerOptions = this }; + var listenOptions = new ListenOptions(socketPath); + ApplyEndpointDefaults(listenOptions); configure(listenOptions); ListenOptions.Add(listenOptions); } @@ -190,7 +277,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core throw new ArgumentNullException(nameof(configure)); } - var listenOptions = new ListenOptions(handle) { KestrelServerOptions = this }; + var listenOptions = new ListenOptions(handle); + ApplyEndpointDefaults(listenOptions); configure(listenOptions); ListenOptions.Add(listenOptions); } diff --git a/src/Kestrel.Core/ListenOptionsHttpsExtensions.cs b/src/Kestrel.Core/ListenOptionsHttpsExtensions.cs index 68f2e71ef1..6a03648746 100644 --- a/src/Kestrel.Core/ListenOptionsHttpsExtensions.cs +++ b/src/Kestrel.Core/ListenOptionsHttpsExtensions.cs @@ -1,9 +1,11 @@ // 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.IO; using System.Security.Cryptography.X509Certificates; using Microsoft.AspNetCore.Server.Kestrel.Core; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal; using Microsoft.AspNetCore.Server.Kestrel.Https; using Microsoft.AspNetCore.Server.Kestrel.Https.Internal; using Microsoft.Extensions.DependencyInjection; @@ -16,18 +18,21 @@ namespace Microsoft.AspNetCore.Hosting /// public static class ListenOptionsHttpsExtensions { + /// + /// Configure Kestrel to use HTTPS with the default certificate if available. + /// This will throw if no default certificate is configured. + /// + /// The to configure. + /// The . + public static ListenOptions UseHttps(this ListenOptions listenOptions) => listenOptions.UseHttps(_ => { }); + /// /// Configure Kestrel to use HTTPS. /// - /// - /// The to configure. - /// - /// - /// The name of a certificate file, relative to the directory that contains the application content files. - /// - /// - /// The . - /// + /// The to configure. + /// The name of a certificate file, relative to the directory that contains the application + /// content files. + /// The . public static ListenOptions UseHttps(this ListenOptions listenOptions, string fileName) { var env = listenOptions.KestrelServerOptions.ApplicationServices.GetRequiredService(); @@ -37,18 +42,11 @@ namespace Microsoft.AspNetCore.Hosting /// /// Configure Kestrel to use HTTPS. /// - /// - /// The to configure. - /// - /// - /// The name of a certificate file, relative to the directory that contains the application content files. - /// - /// - /// The password required to access the X.509 certificate data. - /// - /// - /// The . - /// + /// The to configure. + /// The name of a certificate file, relative to the directory that contains the application + /// content files. + /// The password required to access the X.509 certificate data. + /// The . public static ListenOptions UseHttps(this ListenOptions listenOptions, string fileName, string password) { var env = listenOptions.KestrelServerOptions.ApplicationServices.GetRequiredService(); @@ -58,32 +56,157 @@ namespace Microsoft.AspNetCore.Hosting /// /// Configure Kestrel to use HTTPS. /// - /// - /// The to configure. - /// - /// - /// The X.509 certificate. - /// - /// - /// The . - /// - public static ListenOptions UseHttps(this ListenOptions listenOptions, X509Certificate2 serverCertificate) + /// The to configure. + /// The name of a certificate file, relative to the directory that contains the application content files. + /// The password required to access the X.509 certificate data. + /// An Action to configure the . + /// The . + public static ListenOptions UseHttps(this ListenOptions listenOptions, string fileName, string password, + Action configureOptions) { - return listenOptions.UseHttps(new HttpsConnectionAdapterOptions { ServerCertificate = serverCertificate }); + var env = listenOptions.KestrelServerOptions.ApplicationServices.GetRequiredService(); + return listenOptions.UseHttps(new X509Certificate2(Path.Combine(env.ContentRootPath, fileName), password), configureOptions); } /// /// Configure Kestrel to use HTTPS. /// - /// - /// The to configure. - /// - /// - /// Options to configure HTTPS. - /// - /// - /// The . - /// + /// The to configure. + /// The certificate store to load the certificate from. + /// The subject name for the certificate to load. + /// The . + public static ListenOptions UseHttps(this ListenOptions listenOptions, StoreName storeName, string subject) + => listenOptions.UseHttps(storeName, subject, allowInvalid: false); + + /// + /// Configure Kestrel to use HTTPS. + /// + /// The to configure. + /// The certificate store to load the certificate from. + /// The subject name for the certificate to load. + /// Indicates if invalid certificates should be considered, such as self-signed certificates. + /// The . + public static ListenOptions UseHttps(this ListenOptions listenOptions, StoreName storeName, string subject, bool allowInvalid) + => listenOptions.UseHttps(storeName, subject, allowInvalid, StoreLocation.CurrentUser); + + /// + /// Configure Kestrel to use HTTPS. + /// + /// The to configure. + /// The certificate store to load the certificate from. + /// The subject name for the certificate to load. + /// Indicates if invalid certificates should be considered, such as self-signed certificates. + /// The store location to load the certificate from. + /// The . + public static ListenOptions UseHttps(this ListenOptions listenOptions, StoreName storeName, string subject, bool allowInvalid, StoreLocation location) + => listenOptions.UseHttps(storeName, subject, allowInvalid, location, configureOptions: _ => { }); + + /// + /// Configure Kestrel to use HTTPS. + /// + /// The to configure. + /// The certificate store to load the certificate from. + /// The subject name for the certificate to load. + /// Indicates if invalid certificates should be considered, such as self-signed certificates. + /// The store location to load the certificate from. + /// An Action to configure the . + /// The . + public static ListenOptions UseHttps(this ListenOptions listenOptions, StoreName storeName, string subject, bool allowInvalid, StoreLocation location, + Action configureOptions) + { + return listenOptions.UseHttps(CertificateLoader.LoadFromStoreCert(subject, storeName.ToString(), location, allowInvalid), configureOptions); + } + + /// + /// Configure Kestrel to use HTTPS. + /// + /// The to configure. + /// The X.509 certificate. + /// The . + public static ListenOptions UseHttps(this ListenOptions listenOptions, X509Certificate2 serverCertificate) + { + if (serverCertificate == null) + { + throw new ArgumentNullException(nameof(serverCertificate)); + } + + return listenOptions.UseHttps(options => + { + options.ServerCertificate = serverCertificate; + }); + } + + /// + /// Configure Kestrel to use HTTPS. + /// + /// The to configure. + /// The X.509 certificate. + /// An Action to configure the . + /// The . + public static ListenOptions UseHttps(this ListenOptions listenOptions, X509Certificate2 serverCertificate, + Action configureOptions) + { + if (serverCertificate == null) + { + throw new ArgumentNullException(nameof(serverCertificate)); + } + + if (configureOptions == null) + { + throw new ArgumentNullException(nameof(configureOptions)); + } + + return listenOptions.UseHttps(options => + { + options.ServerCertificate = serverCertificate; + configureOptions(options); + }); + } + + /// + /// Configure Kestrel to use HTTPS. + /// + /// The to configure. + /// An action to configure options for HTTPS. + /// The . + public static ListenOptions UseHttps(this ListenOptions listenOptions, Action configureOptions) + { + if (configureOptions == null) + { + throw new ArgumentNullException(nameof(configureOptions)); + } + + var options = new HttpsConnectionAdapterOptions(); + listenOptions.KestrelServerOptions.ApplyHttpsDefaults(options); + configureOptions(options); + + if (options.ServerCertificate == null) + { + throw new InvalidOperationException(CoreStrings.NoCertSpecifiedNoDevelopmentCertificateFound); + } + return listenOptions.UseHttps(options); + } + + // Use Https if a default cert is available + internal static bool TryUseHttps(this ListenOptions listenOptions) + { + var options = new HttpsConnectionAdapterOptions(); + listenOptions.KestrelServerOptions.ApplyHttpsDefaults(options); + + if (options.ServerCertificate == null) + { + return false; + } + listenOptions.UseHttps(options); + return true; + } + + /// + /// Configure Kestrel to use HTTPS. + /// + /// The to configure. + /// Options to configure HTTPS. + /// The . public static ListenOptions UseHttps(this ListenOptions listenOptions, HttpsConnectionAdapterOptions httpsOptions) { var loggerFactory = listenOptions.KestrelServerOptions.ApplicationServices.GetRequiredService(); diff --git a/src/Kestrel.Core/Properties/AssemblyInfo.cs b/src/Kestrel.Core/Properties/AssemblyInfo.cs index e2d152d0d3..af17db4f34 100644 --- a/src/Kestrel.Core/Properties/AssemblyInfo.cs +++ b/src/Kestrel.Core/Properties/AssemblyInfo.cs @@ -3,6 +3,7 @@ using System.Runtime.CompilerServices; +[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Server.Kestrel.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] [assembly: InternalsVisibleTo("Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.FunctionalTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] [assembly: InternalsVisibleTo("Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.FunctionalTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] [assembly: InternalsVisibleTo("Microsoft.AspNetCore.Server.Kestrel.Core.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] diff --git a/src/Kestrel.Core/Properties/CoreStrings.Designer.cs b/src/Kestrel.Core/Properties/CoreStrings.Designer.cs index 4e1d8cf2c7..ff2732087f 100644 --- a/src/Kestrel.Core/Properties/CoreStrings.Designer.cs +++ b/src/Kestrel.Core/Properties/CoreStrings.Designer.cs @@ -1704,6 +1704,76 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core internal static string FormatServiceCertificateRequired() => GetString("ServiceCertificateRequired"); + /// + /// No listening endpoints were configured. Binding to {address0} and {address1} by default. + /// + internal static string BindingToDefaultAddresses + { + get => GetString("BindingToDefaultAddresses"); + } + + /// + /// No listening endpoints were configured. Binding to {address0} and {address1} by default. + /// + internal static string FormatBindingToDefaultAddresses(object address0, object address1) + => string.Format(CultureInfo.CurrentCulture, GetString("BindingToDefaultAddresses", "address0", "address1"), address0, address1); + + /// + /// The requested certificate {subject} could not be found in {storeLocation}/{storeName} with AllowInvalid setting: {allowInvalid}. + /// + internal static string CertNotFoundInStore + { + get => GetString("CertNotFoundInStore"); + } + + /// + /// The requested certificate {subject} could not be found in {storeLocation}/{storeName} with AllowInvalid setting: {allowInvalid}. + /// + internal static string FormatCertNotFoundInStore(object subject, object storeLocation, object storeName, object allowInvalid) + => string.Format(CultureInfo.CurrentCulture, GetString("CertNotFoundInStore", "subject", "storeLocation", "storeName", "allowInvalid"), subject, storeLocation, storeName, allowInvalid); + + /// + /// The endpoint {endpointName} is missing the required 'Url' parameter. + /// + internal static string EndpointMissingUrl + { + get => GetString("EndpointMissingUrl"); + } + + /// + /// The endpoint {endpointName} is missing the required 'Url' parameter. + /// + internal static string FormatEndpointMissingUrl(object endpointName) + => string.Format(CultureInfo.CurrentCulture, GetString("EndpointMissingUrl", "endpointName"), endpointName); + + /// + /// Unable to configure HTTPS endpoint. No server certificate was specified and the default developer certificate could not be found. Try running 'dotnet developercertificates https -t' to setup a developer certificate for use with localhost. For information on configuring HTTPS see https://go.microsoft.com/fwlink/?linkid=848054 + /// + internal static string NoCertSpecifiedNoDevelopmentCertificateFound + { + get => GetString("NoCertSpecifiedNoDevelopmentCertificateFound"); + } + + /// + /// Unable to configure HTTPS endpoint. No server certificate was specified and the default developer certificate could not be found. Try running 'dotnet developercertificates https -t' to setup a developer certificate for use with localhost. For information on configuring HTTPS see https://go.microsoft.com/fwlink/?linkid=848054 + /// + internal static string FormatNoCertSpecifiedNoDevelopmentCertificateFound() + => GetString("NoCertSpecifiedNoDevelopmentCertificateFound"); + + /// + /// The endpoint {endpointName} specified multiple certificate sources. + /// + internal static string MultipleCertificateSources + { + get => GetString("MultipleCertificateSources"); + } + + /// + /// The endpoint {endpointName} specified multiple certificate sources. + /// + internal static string FormatMultipleCertificateSources(object endpointName) + => string.Format(CultureInfo.CurrentCulture, GetString("MultipleCertificateSources", "endpointName"), endpointName); + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); diff --git a/src/Kestrel/Internal/DefaultHttpsProvider.cs b/src/Kestrel/Internal/DefaultHttpsProvider.cs deleted file mode 100644 index efc7d9fb10..0000000000 --- a/src/Kestrel/Internal/DefaultHttpsProvider.cs +++ /dev/null @@ -1,42 +0,0 @@ -// 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.Linq; -using System.Security.Cryptography.X509Certificates; -using Microsoft.AspNetCore.Certificates.Generation; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Server.Kestrel.Core; -using Microsoft.AspNetCore.Server.Kestrel.Core.Internal; -using Microsoft.Extensions.Logging; - -namespace Microsoft.AspNetCore.Server.Kestrel.Internal -{ - public class DefaultHttpsProvider : IDefaultHttpsProvider - { - private static readonly CertificateManager _certificateManager = new CertificateManager(); - - private readonly ILogger _logger; - - public DefaultHttpsProvider(ILogger logger) - { - _logger = logger; - } - - public void ConfigureHttps(ListenOptions listenOptions) - { - var certificate = _certificateManager.ListCertificates(CertificatePurpose.HTTPS, StoreName.My, StoreLocation.CurrentUser, isValid: true) - .FirstOrDefault(); - if (certificate != null) - { - _logger.LocatedDevelopmentCertificate(certificate); - listenOptions.UseHttps(certificate); - } - else - { - _logger.UnableToLocateDevelopmentCertificate(); - throw new InvalidOperationException(KestrelStrings.HttpsUrlProvidedButNoDevelopmentCertificateFound); - } - } - } -} diff --git a/src/Kestrel/Kestrel.csproj b/src/Kestrel/Kestrel.csproj index ac46ce4d3d..55f910ddcf 100644 --- a/src/Kestrel/Kestrel.csproj +++ b/src/Kestrel/Kestrel.csproj @@ -12,8 +12,6 @@ - - diff --git a/src/Kestrel/KestrelStrings.resx b/src/Kestrel/KestrelStrings.resx deleted file mode 100644 index d39f8b5166..0000000000 --- a/src/Kestrel/KestrelStrings.resx +++ /dev/null @@ -1,123 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 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 - - - Unable to configure HTTPS endpoint. Try running 'dotnet developercertificates https -t' to setup a developer certificate for use with localhost. For information on configuring HTTPS see https://go.microsoft.com/fwlink/?linkid=848054 - - \ No newline at end of file diff --git a/src/Kestrel/Properties/KestrelStrings.Designer.cs b/src/Kestrel/Properties/KestrelStrings.Designer.cs deleted file mode 100644 index 0246bf453a..0000000000 --- a/src/Kestrel/Properties/KestrelStrings.Designer.cs +++ /dev/null @@ -1,44 +0,0 @@ -// -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); - - /// - /// Unable to configure HTTPS endpoint. Try running 'dotnet developercertificates https -t' to setup a developer certificate for use with localhost. For information on configuring HTTPS see https://go.microsoft.com/fwlink/?linkid=848054 - /// - internal static string HttpsUrlProvidedButNoDevelopmentCertificateFound - { - get => GetString("HttpsUrlProvidedButNoDevelopmentCertificateFound"); - } - - /// - /// Unable to configure HTTPS endpoint. Try running 'dotnet developercertificates https -t' to setup a developer certificate for use with localhost. For information on configuring HTTPS see https://go.microsoft.com/fwlink/?linkid=848054 - /// - internal static string FormatHttpsUrlProvidedButNoDevelopmentCertificateFound() - => GetString("HttpsUrlProvidedButNoDevelopmentCertificateFound"); - - 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/Kestrel/WebHostBuilderKestrelExtensions.cs b/src/Kestrel/WebHostBuilderKestrelExtensions.cs index fdc149e2fe..faac901aff 100644 --- a/src/Kestrel/WebHostBuilderKestrelExtensions.cs +++ b/src/Kestrel/WebHostBuilderKestrelExtensions.cs @@ -34,7 +34,6 @@ namespace Microsoft.AspNetCore.Hosting services.AddTransient, KestrelServerOptionsSetup>(); services.AddSingleton(); - services.AddSingleton(); }); } diff --git a/test/Kestrel.Core.Tests/AddressBinderTests.cs b/test/Kestrel.Core.Tests/AddressBinderTests.cs index f2580353db..d548fa70ba 100644 --- a/test/Kestrel.Core.Tests/AddressBinderTests.cs +++ b/test/Kestrel.Core.Tests/AddressBinderTests.cs @@ -54,46 +54,50 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests public void ParseAddressDefaultsToAnyIPOnInvalidIPAddress(string host) { var options = new KestrelServerOptions(); - var listenOptions = AddressBinder.ParseAddress($"http://{host}", options, Mock.Of()); + var listenOptions = AddressBinder.ParseAddress($"http://{host}", out var https); Assert.IsType(listenOptions); Assert.Equal(ListenType.IPEndPoint, listenOptions.Type); Assert.Equal(IPAddress.IPv6Any, listenOptions.IPEndPoint.Address); Assert.Equal(80, listenOptions.IPEndPoint.Port); + Assert.False(https); } [Fact] public void ParseAddressLocalhost() { var options = new KestrelServerOptions(); - var listenOptions = AddressBinder.ParseAddress("http://localhost", options, Mock.Of()); + var listenOptions = AddressBinder.ParseAddress("http://localhost", out var https); Assert.IsType(listenOptions); Assert.Equal(ListenType.IPEndPoint, listenOptions.Type); Assert.Equal(IPAddress.Loopback, listenOptions.IPEndPoint.Address); Assert.Equal(80, listenOptions.IPEndPoint.Port); + Assert.False(https); } [Fact] public void ParseAddressUnixPipe() { var options = new KestrelServerOptions(); - var listenOptions = AddressBinder.ParseAddress("http://unix:/tmp/kestrel-test.sock", options, Mock.Of()); + var listenOptions = AddressBinder.ParseAddress("http://unix:/tmp/kestrel-test.sock", out var https); Assert.Equal(ListenType.SocketPath, listenOptions.Type); Assert.Equal("/tmp/kestrel-test.sock", listenOptions.SocketPath); + Assert.False(https); } [Theory] - [InlineData("http://10.10.10.10:5000/", "10.10.10.10", 5000)] - [InlineData("http://[::1]:5000", "::1", 5000)] - [InlineData("http://[::1]", "::1", 80)] - [InlineData("http://127.0.0.1", "127.0.0.1", 80)] - [InlineData("https://127.0.0.1", "127.0.0.1", 443)] - public void ParseAddressIP(string address, string ip, int port) + [InlineData("http://10.10.10.10:5000/", "10.10.10.10", 5000, false)] + [InlineData("http://[::1]:5000", "::1", 5000, false)] + [InlineData("http://[::1]", "::1", 80, false)] + [InlineData("http://127.0.0.1", "127.0.0.1", 80, false)] + [InlineData("https://127.0.0.1", "127.0.0.1", 443, true)] + public void ParseAddressIP(string address, string ip, int port, bool isHttps) { var options = new KestrelServerOptions(); - var listenOptions = AddressBinder.ParseAddress(address, options, Mock.Of()); + var listenOptions = AddressBinder.ParseAddress(address, out var https); Assert.Equal(ListenType.IPEndPoint, listenOptions.Type); Assert.Equal(IPAddress.Parse(ip), listenOptions.IPEndPoint.Address); Assert.Equal(port, listenOptions.IPEndPoint.Port); + Assert.Equal(isHttps, https); } [Fact] @@ -107,7 +111,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests AddressBinder.BindAsync(addresses, options, NullLogger.Instance, - Mock.Of(), endpoint => throw new AddressInUseException("already in use"))); } @@ -128,7 +131,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests await AddressBinder.BindAsync(addresses, options, logger, - Mock.Of(), endpoint => { if (endpoint.IPEndPoint.Address == IPAddress.IPv6Any) diff --git a/test/Kestrel.Core.Tests/Kestrel.Core.Tests.csproj b/test/Kestrel.Core.Tests/Kestrel.Core.Tests.csproj index ede5868b74..632a9f5ae7 100644 --- a/test/Kestrel.Core.Tests/Kestrel.Core.Tests.csproj +++ b/test/Kestrel.Core.Tests/Kestrel.Core.Tests.csproj @@ -17,6 +17,7 @@ + diff --git a/test/Kestrel.Core.Tests/KestrelServerOptionsTests.cs b/test/Kestrel.Core.Tests/KestrelServerOptionsTests.cs index 2597862f80..75739a4c7b 100644 --- a/test/Kestrel.Core.Tests/KestrelServerOptionsTests.cs +++ b/test/Kestrel.Core.Tests/KestrelServerOptionsTests.cs @@ -29,5 +29,40 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests Assert.True(options.AllowSynchronousIO); } + + [Fact] + public void ConfigureEndpointDefaultsAppliesToNewEndpoints() + { + var options = new KestrelServerOptions(); + options.ListenLocalhost(5000); + + Assert.True(options.ListenOptions[0].NoDelay); + + options.ConfigureEndpointDefaults(opt => + { + opt.NoDelay = false; + }); + + options.Listen(new IPEndPoint(IPAddress.Loopback, 5000), opt => + { + // ConfigureEndpointDefaults runs before this callback + Assert.False(opt.NoDelay); + }); + Assert.False(options.ListenOptions[1].NoDelay); + + options.ListenLocalhost(5000, opt => + { + Assert.False(opt.NoDelay); + opt.NoDelay = true; // Can be overriden + }); + Assert.True(options.ListenOptions[2].NoDelay); + + + options.ListenAnyIP(5000, opt => + { + Assert.False(opt.NoDelay); + }); + Assert.False(options.ListenOptions[3].NoDelay); + } } } \ No newline at end of file diff --git a/test/Kestrel.Core.Tests/KestrelServerTests.cs b/test/Kestrel.Core.Tests/KestrelServerTests.cs index da23da4f19..ab9015cf5f 100644 --- a/test/Kestrel.Core.Tests/KestrelServerTests.cs +++ b/test/Kestrel.Core.Tests/KestrelServerTests.cs @@ -9,9 +9,9 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Hosting.Server.Features; -using Microsoft.AspNetCore.Server.Kestrel.Core.Internal; using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal; using Microsoft.AspNetCore.Testing; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Moq; @@ -21,12 +21,21 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests { public class KestrelServerTests { + private KestrelServerOptions CreateServerOptions() + { + var serverOptions = new KestrelServerOptions(); + serverOptions.ApplicationServices = new ServiceCollection() + .AddLogging() + .BuildServiceProvider(); + return serverOptions; + } + [Fact] public void StartWithInvalidAddressThrows() { var testLogger = new TestApplicationErrorLogger { ThrowOnCriticalErrors = false }; - using (var server = CreateServer(new KestrelServerOptions(), testLogger)) + using (var server = CreateServer(CreateServerOptions(), testLogger)) { server.Features.Get().Addresses.Add("http:/asdf"); @@ -40,34 +49,35 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests [Fact] public void StartWithHttpsAddressConfiguresHttpsEndpoints() { - var mockDefaultHttpsProvider = new Mock(); - - using (var server = CreateServer(new KestrelServerOptions(), mockDefaultHttpsProvider.Object)) + var options = CreateServerOptions(); + options.DefaultCertificate = TestResources.GetTestCertificate(); + using (var server = CreateServer(options)) { server.Features.Get().Addresses.Add("https://127.0.0.1:0"); StartDummyApplication(server); - mockDefaultHttpsProvider.Verify(provider => provider.ConfigureHttps(It.IsAny()), Times.Once); + Assert.True(server.Options.ListenOptions.Any()); + Assert.Contains(server.Options.ListenOptions[0].ConnectionAdapters, adapter => adapter.IsHttps); } } [Fact] public void KestrelServerThrowsUsefulExceptionIfDefaultHttpsProviderNotAdded() { - using (var server = CreateServer(new KestrelServerOptions(), defaultHttpsProvider: null, throwOnCriticalErrors: false)) + using (var server = CreateServer(CreateServerOptions(), throwOnCriticalErrors: false)) { server.Features.Get().Addresses.Add("https://127.0.0.1:0"); var ex = Assert.Throws(() => StartDummyApplication(server)); - Assert.Equal(CoreStrings.UnableToConfigureHttpsBindings, ex.Message); + Assert.Equal(CoreStrings.NoCertSpecifiedNoDevelopmentCertificateFound, ex.Message); } } [Fact] public void KestrelServerDoesNotThrowIfNoDefaultHttpsProviderButNoHttpUrls() { - using (var server = CreateServer(new KestrelServerOptions(), defaultHttpsProvider: null)) + using (var server = CreateServer(CreateServerOptions())) { server.Features.Get().Addresses.Add("http://127.0.0.1:0"); @@ -78,12 +88,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests [Fact] public void KestrelServerDoesNotThrowIfNoDefaultHttpsProviderButManualListenOptions() { - var mockDefaultHttpsProvider = new Mock(); - - var serverOptions = new KestrelServerOptions(); + var serverOptions = CreateServerOptions(); serverOptions.Listen(new IPEndPoint(IPAddress.Loopback, 0)); - using (var server = CreateServer(serverOptions, defaultHttpsProvider: null)) + using (var server = CreateServer(serverOptions)) { server.Features.Get().Addresses.Add("https://127.0.0.1:0"); @@ -322,9 +330,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests return new KestrelServer(Options.Create(options), new MockTransportFactory(), new LoggerFactory(new[] { new KestrelTestLoggerProvider(testLogger) })); } - private static KestrelServer CreateServer(KestrelServerOptions options, IDefaultHttpsProvider defaultHttpsProvider, bool throwOnCriticalErrors = true) + private static KestrelServer CreateServer(KestrelServerOptions options, bool throwOnCriticalErrors = true) { - return new KestrelServer(Options.Create(options), new MockTransportFactory(), new LoggerFactory(new[] { new KestrelTestLoggerProvider(throwOnCriticalErrors) }), defaultHttpsProvider); + return new KestrelServer(Options.Create(options), new MockTransportFactory(), new LoggerFactory(new[] { new KestrelTestLoggerProvider(throwOnCriticalErrors) })); } private static void StartDummyApplication(IServer server) diff --git a/test/Kestrel.FunctionalTests/AddressRegistrationTests.cs b/test/Kestrel.FunctionalTests/AddressRegistrationTests.cs index 84cdb61396..b5a0859c36 100644 --- a/test/Kestrel.FunctionalTests/AddressRegistrationTests.cs +++ b/test/Kestrel.FunctionalTests/AddressRegistrationTests.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Net; using System.Net.NetworkInformation; using System.Net.Sockets; +using System.Security.Cryptography.X509Certificates; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; @@ -17,6 +18,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; +using Microsoft.AspNetCore.Server.Kestrel.Https; using Microsoft.AspNetCore.Testing; using Microsoft.AspNetCore.Testing.xunit; using Microsoft.Extensions.Logging; @@ -189,6 +191,40 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests private Task RegisterAddresses_StaticPort_Success(string addressInput, string[] testUrls) => RunTestWithStaticPort(port => RegisterAddresses_Success($"{addressInput}:{port}", testUrls, port)); + [Fact] + public async Task RegisterHttpAddress_UpradedToHttpsByConfigureEndpointDefaults() + { + var hostBuilder = TransportSelector.GetWebHostBuilder() + .UseKestrel(serverOptions => + { + serverOptions.ConfigureEndpointDefaults(listenOptions => + { + listenOptions.UseHttps(TestResources.GetTestCertificate()); + }); + }) + .ConfigureLogging(_configureLoggingDelegate) + .UseUrls("http://127.0.0.1:0") + .Configure(app => + { + var serverAddresses = app.ServerFeatures.Get(); + app.Run(context => + { + Assert.Single(serverAddresses.Addresses); + return context.Response.WriteAsync(serverAddresses.Addresses.First()); + }); + }); + + using (var host = hostBuilder.Build()) + { + host.Start(); + + var expectedUrl = $"https://127.0.0.1:{host.GetPort()}"; + var response = await HttpClientSlim.GetStringAsync(expectedUrl, validateCertificate: false); + + Assert.Equal(expectedUrl, response); + } + } + private async Task RunTestWithStaticPort(Func test) { var retryCount = 0; @@ -361,13 +397,40 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests return RegisterDefaultServerAddresses_Success(new[] { "http://127.0.0.1:5000", "http://[::1]:5000" }); } - private async Task RegisterDefaultServerAddresses_Success(IEnumerable addresses) + [ConditionalFact] + [PortSupportedCondition(5000)] + [PortSupportedCondition(5001)] + public Task DefaultsServerAddress_BindsToIPv4WithHttps() + { + return RegisterDefaultServerAddresses_Success( + new[] { "http://127.0.0.1:5000", "https://127.0.0.1:5001" }, mockHttps: true); + } + + [ConditionalFact] + [IPv6SupportedCondition] + [PortSupportedCondition(5000)] + [PortSupportedCondition(5001)] + public Task DefaultsServerAddress_BindsToIPv6WithHttps() + { + return RegisterDefaultServerAddresses_Success(new[] { + "http://127.0.0.1:5000", "http://[::1]:5000", + "https://127.0.0.1:5001", "https://[::1]:5001"}, + mockHttps: true); + } + + private async Task RegisterDefaultServerAddresses_Success(IEnumerable addresses, bool mockHttps = false) { var testLogger = new TestApplicationErrorLogger(); var hostBuilder = TransportSelector.GetWebHostBuilder() .ConfigureLogging(_configureLoggingDelegate) - .UseKestrel() + .UseKestrel(options => + { + if (mockHttps) + { + options.DefaultCertificate = new X509Certificate2(TestResources.TestCertificatePath, "testPassword"); + } + }) .ConfigureLogging(builder => builder .AddProvider(new KestrelTestLoggerProvider(testLogger)) .SetMinimumLevel(LogLevel.Debug)) @@ -378,13 +441,19 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests host.Start(); Assert.Equal(5000, host.GetPort()); + + if (mockHttps) + { + Assert.Contains(5001, host.GetPorts()); + } + Assert.Single(testLogger.Messages, log => log.LogLevel == LogLevel.Debug && - string.Equals(CoreStrings.FormatBindingToDefaultAddress(Constants.DefaultServerAddress), - log.Message, StringComparison.Ordinal)); + (string.Equals(CoreStrings.FormatBindingToDefaultAddresses(Constants.DefaultServerAddress, Constants.DefaultServerHttpsAddress), log.Message, StringComparison.Ordinal) + || string.Equals(CoreStrings.FormatBindingToDefaultAddress(Constants.DefaultServerAddress), log.Message, StringComparison.Ordinal))); foreach (var address in addresses) { - Assert.Equal(new Uri(address).ToString(), await HttpClientSlim.GetStringAsync(address)); + Assert.Equal(new Uri(address).ToString(), await HttpClientSlim.GetStringAsync(address, validateCertificate: false)); } } } @@ -933,7 +1002,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests } } - [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] private class PortSupportedConditionAttribute : Attribute, ITestCondition { private readonly int _port; diff --git a/test/Kestrel.FunctionalTests/CertificateLoaderTests.cs b/test/Kestrel.FunctionalTests/CertificateLoaderTests.cs new file mode 100644 index 0000000000..21ad9704ed --- /dev/null +++ b/test/Kestrel.FunctionalTests/CertificateLoaderTests.cs @@ -0,0 +1,64 @@ +// 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.Linq; +using System.Security.Cryptography.X509Certificates; +using Microsoft.AspNetCore.Server.Kestrel.Https.Internal; +using Microsoft.AspNetCore.Testing; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests +{ + public class CertificateLoaderTests + { + private readonly ITestOutputHelper _output; + + public CertificateLoaderTests(ITestOutputHelper output) + { + _output = output; + } + + [Theory] + [InlineData("no_extensions.pfx")] + public void IsCertificateAllowedForServerAuth_AllowWithNoExtensions(string testCertName) + { + var certPath = TestResources.GetCertPath(testCertName); + _output.WriteLine("Loading " + certPath); + var cert = new X509Certificate2(certPath, "testPassword"); + Assert.Empty(cert.Extensions.OfType()); + + Assert.True(CertificateLoader.IsCertificateAllowedForServerAuth(cert)); + } + + [Theory] + [InlineData("eku.server.pfx")] + [InlineData("eku.multiple_usages.pfx")] + public void IsCertificateAllowedForServerAuth_ValidatesEnhancedKeyUsageOnCertificate(string testCertName) + { + var certPath = TestResources.GetCertPath(testCertName); + _output.WriteLine("Loading " + certPath); + var cert = new X509Certificate2(certPath, "testPassword"); + Assert.NotEmpty(cert.Extensions); + var eku = Assert.Single(cert.Extensions.OfType()); + Assert.NotEmpty(eku.EnhancedKeyUsages); + + Assert.True(CertificateLoader.IsCertificateAllowedForServerAuth(cert)); + } + + [Theory] + [InlineData("eku.code_signing.pfx")] + [InlineData("eku.client.pfx")] + public void IsCertificateAllowedForServerAuth_RejectsCertificatesMissingServerEku(string testCertName) + { + var certPath = TestResources.GetCertPath(testCertName); + _output.WriteLine("Loading " + certPath); + var cert = new X509Certificate2(certPath, "testPassword"); + Assert.NotEmpty(cert.Extensions); + var eku = Assert.Single(cert.Extensions.OfType()); + Assert.NotEmpty(eku.EnhancedKeyUsages); + + Assert.False(CertificateLoader.IsCertificateAllowedForServerAuth(cert)); + } + } +} diff --git a/test/Kestrel.FunctionalTests/HttpsTests.cs b/test/Kestrel.FunctionalTests/HttpsTests.cs index 8bc8fb7f60..86403b11b5 100644 --- a/test/Kestrel.FunctionalTests/HttpsTests.cs +++ b/test/Kestrel.FunctionalTests/HttpsTests.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.IO; using System.Net; using System.Net.Security; using System.Net.Sockets; @@ -14,8 +13,11 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Server.Kestrel.Core; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal; using Microsoft.AspNetCore.Server.Kestrel.Https; using Microsoft.AspNetCore.Server.Kestrel.Https.Internal; +using Microsoft.AspNetCore.Server.Kestrel.Internal; using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -26,6 +28,61 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests { public class HttpsTests { + private KestrelServerOptions CreateServerOptions() + { + var serverOptions = new KestrelServerOptions(); + serverOptions.ApplicationServices = new ServiceCollection() + .AddLogging() + .BuildServiceProvider(); + return serverOptions; + } + + [Fact] + public void UseHttpsDefaultsToDefaultCert() + { + var serverOptions = CreateServerOptions(); + var defaultCert = new X509Certificate2(TestResources.TestCertificatePath, "testPassword"); + serverOptions.DefaultCertificate = defaultCert; + + serverOptions.ListenLocalhost(5000, options => + { + options.UseHttps(); + }); + + serverOptions.ListenLocalhost(5001, options => + { + options.UseHttps(opt => + { + Assert.Equal(defaultCert, opt.ServerCertificate); + }); + }); + } + + [Fact] + public void ConfigureHttpsDefaultsOverridesDefaultCert() + { + var serverOptions = CreateServerOptions(); + var defaultCert = new X509Certificate2(TestResources.TestCertificatePath, "testPassword"); + serverOptions.DefaultCertificate = defaultCert; + serverOptions.ConfigureHttpsDefaults(options => + { + Assert.Equal(defaultCert, options.ServerCertificate); + options.ServerCertificate = null; + options.ClientCertificateMode = ClientCertificateMode.RequireCertificate; + }); + serverOptions.ListenLocalhost(5000, options => + { + options.UseHttps(opt => + { + Assert.Null(opt.ServerCertificate); + Assert.Equal(ClientCertificateMode.RequireCertificate, opt.ClientCertificateMode); + + // So UseHttps won't throw + opt.ServerCertificate = defaultCert; + }); + }); + } + [Fact] public async Task EmptyRequestLoggedAsInformation() { @@ -270,10 +327,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests { options.Listen(new IPEndPoint(IPAddress.Loopback, 0), listenOptions => { - listenOptions.UseHttps(new HttpsConnectionAdapterOptions + listenOptions.UseHttps(o => { - ServerCertificate = new X509Certificate2(TestResources.TestCertificatePath, "testPassword"), - HandshakeTimeout = TimeSpan.FromSeconds(1) + o.ServerCertificate = new X509Certificate2(TestResources.TestCertificatePath, "testPassword"); + o.HandshakeTimeout = TimeSpan.FromSeconds(1); }); }); }) diff --git a/test/Kestrel.Tests/ConfigurationReaderTests.cs b/test/Kestrel.Tests/ConfigurationReaderTests.cs new file mode 100644 index 0000000000..ecc7f5e587 --- /dev/null +++ b/test/Kestrel.Tests/ConfigurationReaderTests.cs @@ -0,0 +1,177 @@ +// 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 Microsoft.AspNetCore.Server.Kestrel.Core.Internal; +using Microsoft.Extensions.Configuration; +using Xunit; + +namespace Microsoft.AspNetCore.Server.Kestrel.Tests +{ + public class ConfigurationReaderTests + { + [Fact] + public void ReadCertificatesWhenNoCertificatsSection_ReturnsEmptyCollection() + { + var config = new ConfigurationBuilder().AddInMemoryCollection().Build(); + var reader = new ConfigurationReader(config); + var certificates = reader.Certificates; + Assert.NotNull(certificates); + Assert.False(certificates.Any()); + } + + [Fact] + public void ReadCertificatesWhenEmptyCertificatsSection_ReturnsEmptyCollection() + { + var config = new ConfigurationBuilder().AddInMemoryCollection(new[] + { + new KeyValuePair("Certificates", ""), + }).Build(); + var reader = new ConfigurationReader(config); + var certificates = reader.Certificates; + Assert.NotNull(certificates); + Assert.False(certificates.Any()); + } + + [Fact] + public void ReadCertificatsSection_ReturnsCollection() + { + var config = new ConfigurationBuilder().AddInMemoryCollection(new[] + { + new KeyValuePair("Certificates:FileCert:Path", "/path/cert.pfx"), + new KeyValuePair("Certificates:FileCert:Password", "certpassword"), + new KeyValuePair("Certificates:StoreCert:Subject", "certsubject"), + new KeyValuePair("Certificates:StoreCert:Store", "certstore"), + new KeyValuePair("Certificates:StoreCert:Location", "cetlocation"), + new KeyValuePair("Certificates:StoreCert:AllowInvalid", "true"), + }).Build(); + var reader = new ConfigurationReader(config); + var certificates = reader.Certificates; + Assert.NotNull(certificates); + Assert.Equal(2, certificates.Count); + + var fileCert = certificates["FileCert"]; + Assert.True(fileCert.IsFileCert); + Assert.False(fileCert.IsStoreCert); + Assert.Equal("/path/cert.pfx", fileCert.Path); + Assert.Equal("certpassword", fileCert.Password); + + var storeCert = certificates["StoreCert"]; + Assert.False(storeCert.IsFileCert); + Assert.True(storeCert.IsStoreCert); + Assert.Equal("certsubject", storeCert.Subject); + Assert.Equal("certstore", storeCert.Store); + Assert.Equal("cetlocation", storeCert.Location); + Assert.True(storeCert.AllowInvalid); + } + + [Fact] + public void ReadEndpointsWhenNoEndpointsSection_ReturnsEmptyCollection() + { + var config = new ConfigurationBuilder().AddInMemoryCollection().Build(); + var reader = new ConfigurationReader(config); + var endpoints = reader.Endpoints; + Assert.NotNull(endpoints); + Assert.False(endpoints.Any()); + } + + [Fact] + public void ReadEndpointsWhenEmptyEndpointsSection_ReturnsEmptyCollection() + { + var config = new ConfigurationBuilder().AddInMemoryCollection(new[] + { + new KeyValuePair("Endpoints", ""), + }).Build(); + var reader = new ConfigurationReader(config); + var endpoints = reader.Endpoints; + Assert.NotNull(endpoints); + Assert.False(endpoints.Any()); + } + + [Fact] + public void ReadEndpointWithMissingUrl_Throws() + { + var config = new ConfigurationBuilder().AddInMemoryCollection(new[] + { + new KeyValuePair("Endpoints:End1", ""), + }).Build(); + var reader = new ConfigurationReader(config); + Assert.Throws(() => reader.Endpoints); + } + + [Fact] + public void ReadEndpointWithEmptyUrl_Throws() + { + var config = new ConfigurationBuilder().AddInMemoryCollection(new[] + { + new KeyValuePair("Endpoints:End1:Url", ""), + }).Build(); + var reader = new ConfigurationReader(config); + Assert.Throws(() => reader.Endpoints); + } + + [Fact] + public void ReadEndpointsSection_ReturnsCollection() + { + var config = new ConfigurationBuilder().AddInMemoryCollection(new[] + { + new KeyValuePair("Endpoints:End1:Url", "http://*:5001"), + new KeyValuePair("Endpoints:End2:Url", "https://*:5002"), + new KeyValuePair("Endpoints:End3:Url", "https://*:5003"), + new KeyValuePair("Endpoints:End3:Certificate:Path", "/path/cert.pfx"), + new KeyValuePair("Endpoints:End3:Certificate:Password", "certpassword"), + new KeyValuePair("Endpoints:End4:Url", "https://*:5004"), + new KeyValuePair("Endpoints:End4:Certificate:Subject", "certsubject"), + new KeyValuePair("Endpoints:End4:Certificate:Store", "certstore"), + new KeyValuePair("Endpoints:End4:Certificate:Location", "cetlocation"), + new KeyValuePair("Endpoints:End4:Certificate:AllowInvalid", "true"), + }).Build(); + var reader = new ConfigurationReader(config); + var endpoints = reader.Endpoints; + Assert.NotNull(endpoints); + Assert.Equal(4, endpoints.Count()); + + var end1 = endpoints.First(); + Assert.Equal("End1", end1.Name); + Assert.Equal("http://*:5001", end1.Url); + Assert.NotNull(end1.ConfigSection); + Assert.NotNull(end1.Certificate); + Assert.False(end1.Certificate.ConfigSection.Exists()); + + var end2 = endpoints.Skip(1).First(); + Assert.Equal("End2", end2.Name); + Assert.Equal("https://*:5002", end2.Url); + Assert.NotNull(end2.ConfigSection); + Assert.NotNull(end2.Certificate); + Assert.False(end2.Certificate.ConfigSection.Exists()); + + var end3 = endpoints.Skip(2).First(); + Assert.Equal("End3", end3.Name); + Assert.Equal("https://*:5003", end3.Url); + Assert.NotNull(end3.ConfigSection); + Assert.NotNull(end3.Certificate); + Assert.True(end3.Certificate.ConfigSection.Exists()); + var cert3 = end3.Certificate; + Assert.True(cert3.IsFileCert); + Assert.False(cert3.IsStoreCert); + Assert.Equal("/path/cert.pfx", cert3.Path); + Assert.Equal("certpassword", cert3.Password); + + var end4 = endpoints.Skip(3).First(); + Assert.Equal("End4", end4.Name); + Assert.Equal("https://*:5004", end4.Url); + Assert.NotNull(end4.ConfigSection); + Assert.NotNull(end4.Certificate); + Assert.True(end4.Certificate.ConfigSection.Exists()); + var cert4 = end4.Certificate; + Assert.False(cert4.IsFileCert); + Assert.True(cert4.IsStoreCert); + Assert.Equal("certsubject", cert4.Subject); + Assert.Equal("certstore", cert4.Store); + Assert.Equal("cetlocation", cert4.Location); + Assert.True(cert4.AllowInvalid); + } + } +} diff --git a/test/Kestrel.Tests/Kestrel.Tests.csproj b/test/Kestrel.Tests/Kestrel.Tests.csproj index 5137cd136c..c26558bb08 100644 --- a/test/Kestrel.Tests/Kestrel.Tests.csproj +++ b/test/Kestrel.Tests/Kestrel.Tests.csproj @@ -6,8 +6,14 @@ $(StandardTestTfms) + + + + + + diff --git a/test/Kestrel.Tests/KestrelConfigurationBuilderTests.cs b/test/Kestrel.Tests/KestrelConfigurationBuilderTests.cs new file mode 100644 index 0000000000..bdd9bff4c1 --- /dev/null +++ b/test/Kestrel.Tests/KestrelConfigurationBuilderTests.cs @@ -0,0 +1,213 @@ +// 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.AspNetCore.Hosting; +using Microsoft.AspNetCore.Server.Kestrel.Core; +using Microsoft.AspNetCore.Server.Kestrel.Https; +using Microsoft.AspNetCore.Testing; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Microsoft.AspNetCore.Server.Kestrel.Tests +{ + public class KestrelConfigurationBuilderTests + { + private KestrelServerOptions CreateServerOptions() + { + var serverOptions = new KestrelServerOptions(); + serverOptions.ApplicationServices = new ServiceCollection() + .AddLogging() + .BuildServiceProvider(); + return serverOptions; + } + + [Fact] + public void ConfigureNamedEndpoint_OnlyRunForMatchingConfig() + { + var found = false; + var serverOptions = CreateServerOptions(); + var config = new ConfigurationBuilder().AddInMemoryCollection(new[] + { + new KeyValuePair("Endpoints:Found:Url", "http://*:5001"), + }).Build(); + serverOptions.Configure(config) + .Endpoint("Found", endpointOptions => found = true) + .Endpoint("NotFound", endpointOptions => throw new NotImplementedException()) + .Load(); + + Assert.Single(serverOptions.ListenOptions); + Assert.Equal(5001, serverOptions.ListenOptions[0].IPEndPoint.Port); + + Assert.True(found); + } + + [Fact] + public void ConfigureEndpoint_OnlyRunWhenBuildIsCalled() + { + var run = false; + var serverOptions = CreateServerOptions(); + serverOptions.Configure() + .LocalhostEndpoint(5001, endpointOptions => run = true); + + Assert.Empty(serverOptions.ListenOptions); + + serverOptions.ConfigurationLoader.Load(); + + Assert.Single(serverOptions.ListenOptions); + Assert.Equal(5001, serverOptions.ListenOptions[0].IPEndPoint.Port); + + Assert.True(run); + } + + [Fact] + public void CallBuildTwice_OnlyRunsOnce() + { + var serverOptions = CreateServerOptions(); + var builder = serverOptions.Configure() + .LocalhostEndpoint(5001); + + Assert.Empty(serverOptions.ListenOptions); + Assert.Equal(builder, serverOptions.ConfigurationLoader); + + builder.Load(); + + Assert.Single(serverOptions.ListenOptions); + Assert.Equal(5001, serverOptions.ListenOptions[0].IPEndPoint.Port); + Assert.Null(serverOptions.ConfigurationLoader); + + builder.Load(); + + Assert.Single(serverOptions.ListenOptions); + Assert.Equal(5001, serverOptions.ListenOptions[0].IPEndPoint.Port); + Assert.Null(serverOptions.ConfigurationLoader); + } + + [Fact] + public void Configure_IsReplacable() + { + var run1 = false; + var serverOptions = CreateServerOptions(); + var config1 = new ConfigurationBuilder().AddInMemoryCollection(new[] + { + new KeyValuePair("Endpoints:End1:Url", "http://*:5001"), + }).Build(); + serverOptions.Configure(config1) + .LocalhostEndpoint(5001, endpointOptions => run1 = true); + + Assert.Empty(serverOptions.ListenOptions); + Assert.False(run1); + + var run2 = false; + var config2 = new ConfigurationBuilder().AddInMemoryCollection(new[] + { + new KeyValuePair("Endpoints:End2:Url", "http://*:5002"), + }).Build(); + serverOptions.Configure(config2) + .LocalhostEndpoint(5003, endpointOptions => run2 = true); + + serverOptions.ConfigurationLoader.Load(); + + Assert.Equal(2, serverOptions.ListenOptions.Count); + Assert.Equal(5002, serverOptions.ListenOptions[0].IPEndPoint.Port); + Assert.Equal(5003, serverOptions.ListenOptions[1].IPEndPoint.Port); + + Assert.False(run1); + Assert.True(run2); + } + + [Fact] + public void ConfigureDefaultsAppliesToNewConfigureEndpoints() + { + var serverOptions = CreateServerOptions(); + + serverOptions.ConfigureEndpointDefaults(opt => + { + opt.NoDelay = false; + }); + + serverOptions.ConfigureHttpsDefaults(opt => + { + opt.ServerCertificate = new X509Certificate2(TestResources.TestCertificatePath, "testPassword"); + opt.ClientCertificateMode = ClientCertificateMode.RequireCertificate; + }); + + var ran1 = false; + var ran2 = false; + var config = new ConfigurationBuilder().AddInMemoryCollection(new[] + { + new KeyValuePair("Endpoints:End1:Url", "https://*:5001"), + }).Build(); + serverOptions.Configure(config) + .Endpoint("End1", opt => + { + ran1 = true; + Assert.True(opt.IsHttps); + Assert.NotNull(opt.HttpsOptions.ServerCertificate); + Assert.Equal(ClientCertificateMode.RequireCertificate, opt.HttpsOptions.ClientCertificateMode); + Assert.False(opt.ListenOptions.NoDelay); + }) + .LocalhostEndpoint(5002, opt => + { + ran2 = true; + Assert.False(opt.NoDelay); + }) + .Load(); + + Assert.True(ran1); + Assert.True(ran2); + + Assert.NotNull(serverOptions.ListenOptions[0].ConnectionAdapters.Where(adapter => adapter.IsHttps).SingleOrDefault()); + Assert.Null(serverOptions.ListenOptions[1].ConnectionAdapters.Where(adapter => adapter.IsHttps).SingleOrDefault()); + } + + [Fact] + public void ConfigureEndpointDefaultCanEnableHttps() + { + var serverOptions = CreateServerOptions(); + + serverOptions.ConfigureEndpointDefaults(opt => + { + opt.NoDelay = false; + opt.UseHttps(new X509Certificate2(TestResources.TestCertificatePath, "testPassword")); + }); + + serverOptions.ConfigureHttpsDefaults(opt => + { + opt.ClientCertificateMode = ClientCertificateMode.RequireCertificate; + }); + + var ran1 = false; + var ran2 = false; + var config = new ConfigurationBuilder().AddInMemoryCollection(new[] + { + new KeyValuePair("Endpoints:End1:Url", "https://*:5001"), + }).Build(); + serverOptions.Configure(config) + .Endpoint("End1", opt => + { + ran1 = true; + Assert.True(opt.IsHttps); + Assert.Equal(ClientCertificateMode.RequireCertificate, opt.HttpsOptions.ClientCertificateMode); + Assert.False(opt.ListenOptions.NoDelay); + }) + .LocalhostEndpoint(5002, opt => + { + ran2 = true; + Assert.False(opt.NoDelay); + }) + .Load(); + + Assert.True(ran1); + Assert.True(ran2); + + // You only get Https once per endpoint. + Assert.NotNull(serverOptions.ListenOptions[0].ConnectionAdapters.Where(adapter => adapter.IsHttps).SingleOrDefault()); + Assert.NotNull(serverOptions.ListenOptions[1].ConnectionAdapters.Where(adapter => adapter.IsHttps).SingleOrDefault()); + } + } +} diff --git a/test/SystemdActivation/docker-entrypoint.sh b/test/SystemdActivation/docker-entrypoint.sh index 2b501ef6ae..cb8d2f2d6f 100644 --- a/test/SystemdActivation/docker-entrypoint.sh +++ b/test/SystemdActivation/docker-entrypoint.sh @@ -3,10 +3,10 @@ set -e cd /publish -systemd-socket-activate -l 8080 -E ASPNETCORE_BASE_PORT=7000 dotnet SampleApp.dll & +systemd-socket-activate -l 8080 -E ASPNETCORE_BASE_PORT=7000 dotnet SystemdTestApp.dll & socat TCP-LISTEN:8081,fork TCP-CONNECT:127.0.0.1:7000 & socat TCP-LISTEN:8082,fork TCP-CONNECT:127.0.0.1:7001 & -systemd-socket-activate -l /tmp/activate-kestrel.sock -E ASPNETCORE_BASE_PORT=7100 dotnet SampleApp.dll & +systemd-socket-activate -l /tmp/activate-kestrel.sock -E ASPNETCORE_BASE_PORT=7100 dotnet SystemdTestApp.dll & socat TCP-LISTEN:8083,fork UNIX-CLIENT:/tmp/activate-kestrel.sock & socat TCP-LISTEN:8084,fork TCP-CONNECT:127.0.0.1:7100 & socat TCP-LISTEN:8085,fork TCP-CONNECT:127.0.0.1:7101 & diff --git a/test/SystemdActivation/docker.sh b/test/SystemdActivation/docker.sh index 1f4a62b3a6..28560cf69f 100644 --- a/test/SystemdActivation/docker.sh +++ b/test/SystemdActivation/docker.sh @@ -4,14 +4,14 @@ set -e scriptDir=$(dirname "${BASH_SOURCE[0]}") PATH="$HOME/.dotnet/:$PATH" -dotnet publish -f netcoreapp2.0 ./samples/SampleApp/ -cp -R ./samples/SampleApp/bin/Debug/netcoreapp2.0/publish/ $scriptDir +dotnet publish -f netcoreapp2.0 ./samples/SystemdTestApp/ +cp -R ./samples/SystemdTestApp/bin/Debug/netcoreapp2.0/publish/ $scriptDir cp -R ~/.dotnet/ $scriptDir image=$(docker build -qf $scriptDir/Dockerfile $scriptDir) container=$(docker run -Pd $image) -# Try to connect to SampleApp once a second up to 10 times via all available ports. +# Try to connect to SystemdTestApp once a second up to 10 times via all available ports. for i in {1..10}; do curl -f http://$(docker port $container 8080/tcp) \ && curl -f http://$(docker port $container 8081/tcp) \ diff --git a/test/shared/TestResources.cs b/test/shared/TestResources.cs index 6b0cf70fec..b8ae46d18f 100644 --- a/test/shared/TestResources.cs +++ b/test/shared/TestResources.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.IO; +using System.Security.Cryptography.X509Certificates; namespace Microsoft.AspNetCore.Testing { @@ -11,5 +12,10 @@ namespace Microsoft.AspNetCore.Testing public static string TestCertificatePath { get; } = Path.Combine(_baseDir, "testCert.pfx"); public static string GetCertPath(string name) => Path.Combine(_baseDir, name); + + public static X509Certificate2 GetTestCertificate() + { + return new X509Certificate2(TestCertificatePath, "testPassword"); + } } }