-
Notifications
You must be signed in to change notification settings - Fork 523
Add "zero config" HTTPS support using local development certificate. #2093
Conversation
src/Kestrel/DefaultHttpsProvider.cs
Outdated
store.Open(OpenFlags.ReadOnly); | ||
|
||
var certificates = store.Certificates.OfType<X509Certificate2>(); | ||
var certificate = certificates |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You need to check that it has an OID, a private key and hasn't expired
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When do you think you will have dotnet/extensions#263 merged? We shouldn't replicate all that logic here. I should be able to get the certificate by calling CertificateManager.ListCertificates(CertificatePurpose.HTTPS, StoreName.My, StoreLocation.CurrentUser, isValid: true)
, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm working on it
I don't see this as a breaking change. At most a behavior change, and you are weakening the precondition (allowing something that wasn't allowed before) so I don't think this is a breaking change. |
To me this looks like a minor breaking change, but an acceptable one. If you specify https for your URL, your app would fail to start before this. Now it'll do what you (presumably) wanted anyway. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a guide on how to setup an X509Store for my current user so I can test this out locally?
src/Kestrel.Core/KestrelServer.cs
Outdated
@@ -52,6 +54,8 @@ internal KestrelServer(ITransportFactory transportFactory, ServiceContext servic | |||
Features = new FeatureCollection(); | |||
_serverAddresses = new ServerAddressesFeature(); | |||
Features.Set(_serverAddresses); | |||
|
|||
_defaultHttpsProvider = serviceContext.ServerOptions.ApplicationServices.GetService<IDefaultHttpsProvider>(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't call GetService for this. Just make IDefaultHttpsProvider a param to the public ctor.
@@ -19,7 +19,9 @@ internal class AddressBinder | |||
{ | |||
public static async Task BindAsync(IServerAddressesFeature addresses, | |||
List<ListenOptions> listenOptions, | |||
KestrelServerOptions serverOptions, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It doesn't look like this is actually used by the DefaultHttpsProvider or anything else. I see that you are setting this on ListenOptions before calling ConfigureHttps, but I don't see why that's necessary. Save some lines of code and don't bother plumbing this through here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It blows up here:
var loggerFactory = listenOptions.KestrelServerOptions.ApplicationServices.GetRequiredService<ILoggerFactory>(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I may have misunderstood what you said. There's indeed no need to pass it as an arg to BindAsync
, since I can get the KestrelServerOptions
instance from one of the ListenOptions
in the list. I still need that object though, otherwise a NRE is thrown at the line above.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right. I forgot about the call to UseHttps().
@halter73 To get the test cert set up on your machine, clone this PR dotnet/extensions#263 and run the tests. @javiercn can give you more info. |
acca4ee
to
c714e4a
Compare
@CesarBS Couldn't you just add a second ctor, and have the original ctor call the new one with a null |
Yeah just do what @halter73 suggested and there's no breaking change at all in terms of source compat, binary compat, runtime behavior, etc. It's nearly always fine to throw fewer exceptions and be more permissive. |
@@ -31,7 +32,9 @@ internal class AddressBinder | |||
{ | |||
Addresses = addresses.Addresses, | |||
ListenOptions = listenOptions, | |||
ServerOptions = listenOptions.FirstOrDefault()?.KestrelServerOptions, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is pretty gross
New/updated sample? |
if (https) | ||
{ | ||
options.KestrelServerOptions = context.ServerOptions; | ||
context.DefaultHttpsProvider?.ConfigureHttps(options); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We want this to throw if DefaultHttpsProvider is null, no? You asked for https and you're not getting it.
src/Kestrel/DefaultHttpsProvider.cs
Outdated
|
||
if (certificate == null) | ||
{ | ||
throw new InvalidOperationException("Unable to find ASP.NET Core development certificate."); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This level of magic deserves an FW link explaining what to do when it fails.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@danroth27 @javiercn Do we have an FW link already?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good regarding breaking changes (that there aren't any). Had one unrelated comment.
src/Kestrel/DefaultHttpsProvider.cs
Outdated
|
||
var certificates = store.Certificates.OfType<X509Certificate2>(); | ||
var certificate = certificates | ||
.FirstOrDefault(c => HasOid(c, AspNetHttpsOid) && !IsExpired(c) /*&& HasPrivateKey(c)*/); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the commented code needed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. Thanks for catching that, I forgot I had commented it out while testing something.
ae922be
to
256e486
Compare
Updated to use https://github.com/aspnet/Common/blob/b9bc076c782af4a9664b34b561fd91d37e6edda4/shared/Microsoft.AspNetCore.Certificates.Generation.Sources/CertificateManager.cs to find development certificate. |
src/Kestrel/Kestrel.csproj
Outdated
@@ -12,10 +12,13 @@ | |||
|
|||
<ItemGroup> | |||
<PackageReference Include="Microsoft.AspNetCore.Hosting" /> | |||
<PackageReference Include="Microsoft.AspNetCore.Certificates.Generation.Sources" PrivateAssets="All" /> | |||
<PackageReference Include="System.Security.Cryptography.Cng" /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's this for?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
He’s using the same code we have in tooling to locate the certificate (that’s why it’s a shared source library)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need that reference for the RSACng
type.
256e486
to
411af38
Compare
Rebased. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you don't have time to do this before you leave, I can take it over. One small testing comment and that's it.
$"HTTPS endpoints can only be configured using {nameof(KestrelServerOptions)}.{nameof(KestrelServerOptions.Listen)}().", | ||
exception.Message); | ||
Assert.Equal(1, testLogger.CriticalErrorsLogged); | ||
mockDefaultHttpsProvider.Verify(provider => provider.ConfigureHttps(It.IsAny<ListenOptions>()), Times.Once); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not a fan of tests that verify that a certain method was called. Is there a way to build a simple dummy DefaultHttpsProvider and then verify the certificate on the ListenOptions?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is the issue with this kind of test? To test with DefaultHttpsProvider
, I'd have to make the test project depend on the Kestrel
project, instead of just Kestrel.Core
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm. I'll take a bit of a look at it and see if we can make something work. Tests that use mock verification tend to be brittle whenever you change the implementation and they aren't actually testing the desired behavior of the function, they're just testing that the code does what is written. It's a little like a comment over x += 1
that says // Adds 1 to x
:). If that's all that we can do, then I suppose it's OK, but I'd like to try and avoid it.
36f52b2
to
77d7952
Compare
🆙 📅 Using CertificateManager shared-source package now. Also, added some more logging and exception messages. |
src/Kestrel/KestrelStrings.resx
Outdated
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> | ||
</resheader> | ||
<data name="HttpsUrlProvidedButNoDevelopmentCertificateFound" xml:space="preserve"> | ||
<value>An 'https' URL was provided, but a development certificate could not be found.</value> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you add something along the lines of the text that I sent you on email and add the forwarding link that @danroth27 provided?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Everything looks good AFAIK. But we need to get a more descriptive error message when the certificate is not present.
We need to mention what gesture to perform from the command line and a forwarding link to the documentation.
[Fact] | ||
public void KestrelServerThrowsUsefulExceptionIfDefaultHttpsProviderNotAdded() | ||
{ | ||
var mockDefaultHttpsProvider = new Mock<IDefaultHttpsProvider>(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: It doesn't look like mockDefaultHttpsProvider is used in this or the following two test methods.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yep, copy🍝
@@ -274,6 +315,11 @@ private static KestrelServer CreateServer(KestrelServerOptions options, ILogger | |||
return new KestrelServer(Options.Create(options), new MockTransportFactory(), new LoggerFactory(new[] { new KestrelTestLoggerProvider(testLogger) })); | |||
} | |||
|
|||
private static KestrelServer CreateServer(KestrelServerOptions options, IDefaultHttpsProvider defaultHttpsProvider) | |||
{ | |||
return new KestrelServer(Options.Create(options), new MockTransportFactory(), new LoggerFactory(new[] { new KestrelTestLoggerProvider(Mock.Of<ILogger>()) }), defaultHttpsProvider); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just use the default ctor for KestrelTestLoggerProvider. It makes it easier to see tracing when running random tests and should fail test runs if a critical error is logged through the use of TestApplicationErrorLogger.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes sense
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well. After changing that I'm now getting this error:
Failed KestrelServerThrowsUsefulExceptionIfDefaultHttpsProviderNotAdded
Error Message:
Assert.Throws() Failure
Expected: typeof(System.InvalidOperationException)
Actual: typeof(System.AggregateException): An error occurred while writing to logger(s).
Stack Trace:
at Microsoft.Extensions.Logging.Logger.Log[TState](LogLevel logLevel, EventId eventId, TState state, Exception exception, Func`3 formatter)
at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.KestrelTrace.Log[TState](LogLevel logLevel, EventId eventId, TState state, Exception exception, Func`3 formatter) in C:\Users\anurse\Code\aspnet\KestrelHttpServer\src\Kestrel.Core\Internal\Infrastructure\KestrelTrace.cs:line 198
at Microsoft.Extensions.Logging.LoggerExtensions.LogCritical(ILogger logger, EventId eventId, Exception exception, String message, Object[] args)
at Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServer.<StartAsync>d__24`1.MoveNext() in C:\Users\anurse\Code\aspnet\KestrelHttpServer\src\Kestrel.Core\KestrelServer.cs:line 166
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.GetResult()
at Microsoft.AspNetCore.Server.Kestrel.Core.Tests.KestrelServerTests.StartDummyApplication(IServer server) in C:\Users\anurse\Code\aspnet\KestrelHttpServer\test\Kestrel.Core.Tests\KestrelServerTests.cs:line 321
at Microsoft.AspNetCore.Server.Kestrel.Core.Tests.KestrelServerTests.<>c__DisplayClass2_0.<KestrelServerThrowsUsefulExceptionIfDefaultHttpsProviderNotAdded>b__0() in C:\Users\anurse\Code\aspnet\KestrelHttpServer\test\Kestrel.Core.Tests\KestrelServerTests.cs:line 62
I think I'm going to put it back for now :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, it's because the default ctor uses TestApplicationErrorLogger which throws an exception when a Critical message is logged by default. I'm going to change the test to use a NullLogger instance though instead of the Mock.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I changed KestrelTestLoggerProvider
to set ThrowOnCriticalErrors
to false
, since we don't seem to use that behavior anywhere.
|
||
public void ConfigureHttps(ListenOptions listenOptions) | ||
{ | ||
var cert = DefaultCertificateResolver(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why the Func? Was this replaced in a test at one point? It seems like you could just call FindDevelopmentCertificate directly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I was wondering that too. I suppose so that you could construct it and replace the func, but that doesn't really make sense now that it's pubternal rather than public. It's also a simple enough interface to implement. I'll yank the func.
.FirstOrDefault(); | ||
if (certificate != null) | ||
{ | ||
_logger.LogDebug("Using development certificate: {certificateSubjectName} (Thumbprint: {certificateThumbprint})", certificate.Subject, certificate.Thumbprint); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For common/expected logs, I think it's best to use resource strings.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, we use literals for all the other logs (see KestrelTrace) so I wasn't sure exactly what the right pattern was here. I thought about adding some LoggerMessage.Define
action here but even with that, I've not seen our code use resource strings for that. I guess we could though.
} | ||
else | ||
{ | ||
_logger.LogDebug("Development certificate could not be found"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this be given a higher log level? I'm assuming this would normally cause the application to crash on startup, but in the event the exception is somehow swallowed, I think it's a good idea to log this as an error.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, an exception will be thrown by ConfigureHttps
, which calls this. But when refactoring to remove the delegate, I'll probably just do exactly as you suggest here :).
🆙 📅 Responded to feedback, including @javiercn 's comments re messaging. |
@@ -11,7 +11,7 @@ public class KestrelTestLoggerProvider : ILoggerProvider | |||
private readonly ILogger _testLogger; | |||
|
|||
public KestrelTestLoggerProvider() | |||
: this(new TestApplicationErrorLogger()) | |||
: this(new TestApplicationErrorLogger() { ThrowOnCriticalErrors = false }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't do this for all tests. For tests where you are expecting a critical errors to be logged, I would just call new KestrelTestLoggerProvider(new TestApplicationErrorLogger() { ThrowOnCriticalErrors = false })
.
The idea is that most tests should cause critical errors to be logged, so having this can help us catch issues that might have otherwise been missed.
🆙 📅 resolved last few pieces of feedback |
This change enables automatic HTTPS using a development certificate found in the user store. Any URLs containing the https scheme will be configured with that certificate.
Paging @Eilon since this is a breaking change (previously we would throw upon seeing an https URL).
So now if you set
ASPNETCORE_URLS
(or useUseUrls()
) to e.g.https://localhost:5001
, you'll get HTTPS if you add Kestrel with no listen options i.e.This turned out to be more convoluted than I anticipated. I'm hoping someone can suggest a simpler way to achieve the same result.