|
|
@@ -894,7 +894,7 @@ public class KestrelConfigurationLoaderTests
|
|
|
|
|
|
if (reloadOnChange)
|
|
|
{
|
|
|
- await fileTcs.Task.DefaultTimeout();
|
|
|
+ await fileTcs.Task.TimeoutAfter(TimeSpan.FromSeconds(10)); // Needs to be meaningfully longer than the polling period - 4 seconds
|
|
|
}
|
|
|
else
|
|
|
{
|
|
|
@@ -922,6 +922,94 @@ public class KestrelConfigurationLoaderTests
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ [ConditionalFact]
|
|
|
+ [OSSkipCondition(OperatingSystems.Windows)] // Windows has poor support for directory symlinks (e.g. https://github.com/dotnet/runtime/issues/27826)
|
|
|
+ public async Task CertificateChangedOnDisk_Symlink()
|
|
|
+ {
|
|
|
+ var tempDir = Directory.CreateTempSubdirectory().FullName;
|
|
|
+
|
|
|
+ try
|
|
|
+ {
|
|
|
+ // temp/
|
|
|
+ // tls.key -> link/tls.key
|
|
|
+ // link/ -> old/
|
|
|
+ // old/
|
|
|
+ // tls.key
|
|
|
+ // new/
|
|
|
+ // tls.key
|
|
|
+
|
|
|
+ var oldDir = Directory.CreateDirectory(Path.Combine(tempDir, "old"));
|
|
|
+ var newDir = Directory.CreateDirectory(Path.Combine(tempDir, "new"));
|
|
|
+ var oldCertPath = Path.Combine(oldDir.FullName, "tls.key");
|
|
|
+ var newCertPath = Path.Combine(newDir.FullName, "tls.key");
|
|
|
+
|
|
|
+ var dirLink = Directory.CreateSymbolicLink(Path.Combine(tempDir, "link"), "./old");
|
|
|
+ var fileLink = File.CreateSymbolicLink(Path.Combine(tempDir, "tls.key"), "./link/tls.key");
|
|
|
+
|
|
|
+ var serverOptions = CreateServerOptions();
|
|
|
+
|
|
|
+ var certificatePassword = "1234";
|
|
|
+
|
|
|
+ var oldCertificate = new X509Certificate2(TestResources.GetCertPath("aspnetdevcert.pfx"), "testPassword", X509KeyStorageFlags.Exportable);
|
|
|
+ var oldCertificateBytes = oldCertificate.Export(X509ContentType.Pkcs12, certificatePassword);
|
|
|
+
|
|
|
+ File.WriteAllBytes(oldCertPath, oldCertificateBytes);
|
|
|
+
|
|
|
+ var newCertificate = new X509Certificate2(TestResources.TestCertificatePath, "testPassword", X509KeyStorageFlags.Exportable);
|
|
|
+ var newCertificateBytes = newCertificate.Export(X509ContentType.Pkcs12, certificatePassword);
|
|
|
+
|
|
|
+ File.WriteAllBytes(newCertPath, newCertificateBytes);
|
|
|
+
|
|
|
+ var endpointConfigurationCallCount = 0;
|
|
|
+ var config = new ConfigurationBuilder().AddInMemoryCollection(new[]
|
|
|
+ {
|
|
|
+ new KeyValuePair<string, string>("Endpoints:End1:Url", "https://*:5001"),
|
|
|
+ new KeyValuePair<string, string>("Endpoints:End1:Certificate:Path", fileLink.FullName),
|
|
|
+ new KeyValuePair<string, string>("Endpoints:End1:Certificate:Password", certificatePassword),
|
|
|
+ }).Build();
|
|
|
+
|
|
|
+ var configLoader = serverOptions
|
|
|
+ .Configure(config, reloadOnChange: true)
|
|
|
+ .Endpoint("End1", opt =>
|
|
|
+ {
|
|
|
+ Assert.True(opt.IsHttps);
|
|
|
+ var expectedSerialNumber = endpointConfigurationCallCount == 0
|
|
|
+ ? oldCertificate.SerialNumber
|
|
|
+ : newCertificate.SerialNumber;
|
|
|
+ Assert.Equal(opt.HttpsOptions.ServerCertificate.SerialNumber, expectedSerialNumber);
|
|
|
+ endpointConfigurationCallCount++;
|
|
|
+ });
|
|
|
+
|
|
|
+ configLoader.Load();
|
|
|
+
|
|
|
+ var fileTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
|
+
|
|
|
+ configLoader.GetReloadToken().RegisterChangeCallback(_ => fileTcs.SetResult(), state: null);
|
|
|
+
|
|
|
+ // Clobber link/ directory symlink - this will effectively cause the cert to be updated.
|
|
|
+ // Unfortunately, it throws (file exists) if we don't delete the old one first so it's not a single, clean FS operation.
|
|
|
+ dirLink.Delete();
|
|
|
+ dirLink = Directory.CreateSymbolicLink(Path.Combine(tempDir, "link"), "./new");
|
|
|
+
|
|
|
+ // This can fail in local runs where the timeout is 5 seconds and polling period is 4 seconds - just re-run
|
|
|
+ await fileTcs.Task.DefaultTimeout();
|
|
|
+
|
|
|
+ Assert.Equal(1, endpointConfigurationCallCount);
|
|
|
+
|
|
|
+ configLoader.Reload();
|
|
|
+
|
|
|
+ Assert.Equal(2, endpointConfigurationCallCount);
|
|
|
+ }
|
|
|
+ finally
|
|
|
+ {
|
|
|
+ if (Directory.Exists(tempDir))
|
|
|
+ {
|
|
|
+ // Note: the watcher will see this event, but we ignore deletions, so it shouldn't matter
|
|
|
+ Directory.Delete(tempDir, recursive: true);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
[ConditionalTheory]
|
|
|
[InlineData("http1", HttpProtocols.Http1)]
|
|
|
// [InlineData("http2", HttpProtocols.Http2)] // Not supported due to missing ALPN support. https://github.com/dotnet/corefx/issues/33016
|