From ff236d4d1197f0a7aa1f1aae162877bf13f4e36c Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Mon, 19 May 2025 04:41:27 +0000 Subject: [PATCH 1/4] Do not fail on partial trust warning. --- .../Certificates/CertificateService.cs | 16 +++++ .../Certificates/CertificateServiceTests.cs | 67 +++++++++++++++++++ 2 files changed, 83 insertions(+) create mode 100644 tests/Aspire.Cli.Tests/Certificates/CertificateServiceTests.cs diff --git a/src/Aspire.Cli/Certificates/CertificateService.cs b/src/Aspire.Cli/Certificates/CertificateService.cs index 368bf57079f..b0abf5ac32a 100644 --- a/src/Aspire.Cli/Certificates/CertificateService.cs +++ b/src/Aspire.Cli/Certificates/CertificateService.cs @@ -50,11 +50,27 @@ public async Task EnsureCertificatesTrustedAsync(IDotNetCliRunner runner, Cancel if (trustExitCode != 0) { + var outputLines = ensureCertificateCollector.GetLines(); + + if (outputLines.Any(line => line.Line == DevCertsPartialTrustMessage)) + { + // On some platforms the trust command may return with a non-zero exit code by still + // be functional enough to work for .NET Aspire. This is a workaround for that non-zero + // exit code that allows the CLI to continue starting up the apphost. We want to warn + // when this happens so we know we are hitting this corner case. + interactionService.DisplayMessage( + "warning", + "The HTTPS developer certificate is partially trusted. Some clients may not work correctly."); + return; + } + interactionService.DisplayLines(ensureCertificateCollector.GetLines()); throw new CertificateServiceException($"Failed to trust certificates, trust command failed with exit code: {trustExitCode}"); } } } + + private const string DevCertsPartialTrustMessage = "There was an error trusting the HTTPS developer certificate. It will be trusted by some clients but not by others."; } public sealed class CertificateServiceException(string message) : Exception(message) diff --git a/tests/Aspire.Cli.Tests/Certificates/CertificateServiceTests.cs b/tests/Aspire.Cli.Tests/Certificates/CertificateServiceTests.cs new file mode 100644 index 00000000000..55e905138da --- /dev/null +++ b/tests/Aspire.Cli.Tests/Certificates/CertificateServiceTests.cs @@ -0,0 +1,67 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire.Cli.Certificates; +using Aspire.Cli.Tests.TestServices; +using Aspire.Cli.Tests.Utils; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Aspire.Cli.Tests.Certificates; + +public class CertificateServiceTests(ITestOutputHelper outputHelper) +{ + [Fact] + public async Task EnsureCertificatesTrustedAsyncSucceedsOnExitCode4IfPartialTrustMessageDetected() + { + var services = CliTestHelper.CreateServiceCollection(outputHelper, options => + { + options.DotNetCliRunnerFactory = sp => + { + var runner = new TestDotNetCliRunner(); + runner.CheckHttpCertificateAsyncCallback = (_, _) => 1; + runner.TrustHttpCertificateAsyncCallback = (options, _) => + { + Assert.NotNull(options.StandardErrorCallback); + options.StandardErrorCallback!.Invoke("There was an error trusting the HTTPS developer certificate. It will be trusted by some clients but not by others."); + return 4; + }; + return runner; + }; + }); + + var sp = services.BuildServiceProvider(); + var cs = sp.GetRequiredService(); + var runner = sp.GetRequiredService(); + + // If this does not throw then the code is behaving correctly. + await cs.EnsureCertificatesTrustedAsync(runner, TestContext.Current.CancellationToken).WaitAsync(CliTestConstants.DefaultTimeout); + } + + [Fact] + public async Task EnsureCertificatesTrustedAsyncFailsOnExitCode4IfPartialTrustMessageNotDetected() + { + var services = CliTestHelper.CreateServiceCollection(outputHelper, options => + { + options.DotNetCliRunnerFactory = sp => + { + var runner = new TestDotNetCliRunner(); + runner.CheckHttpCertificateAsyncCallback = (_, _) => 1; + runner.TrustHttpCertificateAsyncCallback = (options, _) => 4; + return runner; + }; + }); + + var sp = services.BuildServiceProvider(); + var cs = sp.GetRequiredService(); + var runner = sp.GetRequiredService(); + + var ex = await Assert.ThrowsAsync(async () => + { + await cs.EnsureCertificatesTrustedAsync(runner, TestContext.Current.CancellationToken).WaitAsync(CliTestConstants.DefaultTimeout); + + }); + + Assert.Equal("Failed to trust certificates, trust command failed with exit code: 4", ex.Message); + } +} \ No newline at end of file From 00660043a921749ff04cb8b30737b4f5682fa798 Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Mon, 19 May 2025 04:53:15 +0000 Subject: [PATCH 2/4] Reuse message in tests. --- src/Aspire.Cli/Certificates/CertificateService.cs | 2 +- tests/Aspire.Cli.Tests/Certificates/CertificateServiceTests.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Aspire.Cli/Certificates/CertificateService.cs b/src/Aspire.Cli/Certificates/CertificateService.cs index b0abf5ac32a..8210cfc3ea1 100644 --- a/src/Aspire.Cli/Certificates/CertificateService.cs +++ b/src/Aspire.Cli/Certificates/CertificateService.cs @@ -70,7 +70,7 @@ public async Task EnsureCertificatesTrustedAsync(IDotNetCliRunner runner, Cancel } } - private const string DevCertsPartialTrustMessage = "There was an error trusting the HTTPS developer certificate. It will be trusted by some clients but not by others."; + internal const string DevCertsPartialTrustMessage = "There was an error trusting the HTTPS developer certificate. It will be trusted by some clients but not by others."; } public sealed class CertificateServiceException(string message) : Exception(message) diff --git a/tests/Aspire.Cli.Tests/Certificates/CertificateServiceTests.cs b/tests/Aspire.Cli.Tests/Certificates/CertificateServiceTests.cs index 55e905138da..a728f10b212 100644 --- a/tests/Aspire.Cli.Tests/Certificates/CertificateServiceTests.cs +++ b/tests/Aspire.Cli.Tests/Certificates/CertificateServiceTests.cs @@ -23,7 +23,7 @@ public async Task EnsureCertificatesTrustedAsyncSucceedsOnExitCode4IfPartialTrus runner.TrustHttpCertificateAsyncCallback = (options, _) => { Assert.NotNull(options.StandardErrorCallback); - options.StandardErrorCallback!.Invoke("There was an error trusting the HTTPS developer certificate. It will be trusted by some clients but not by others."); + options.StandardErrorCallback!.Invoke(CertificateService.DevCertsPartialTrustMessage); return 4; }; return runner; From 7fb19941f33d002bd1a8174191f60825bee4c191 Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Mon, 19 May 2025 05:13:12 +0000 Subject: [PATCH 3/4] Use exit code as well. --- .../Certificates/CertificateService.cs | 28 +++++++++---------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/src/Aspire.Cli/Certificates/CertificateService.cs b/src/Aspire.Cli/Certificates/CertificateService.cs index 8210cfc3ea1..d8588f36b15 100644 --- a/src/Aspire.Cli/Certificates/CertificateService.cs +++ b/src/Aspire.Cli/Certificates/CertificateService.cs @@ -48,22 +48,20 @@ public async Task EnsureCertificatesTrustedAsync(IDotNetCliRunner runner, Cancel options, cancellationToken)); - if (trustExitCode != 0) - { - var outputLines = ensureCertificateCollector.GetLines(); - - if (outputLines.Any(line => line.Line == DevCertsPartialTrustMessage)) - { - // On some platforms the trust command may return with a non-zero exit code by still - // be functional enough to work for .NET Aspire. This is a workaround for that non-zero - // exit code that allows the CLI to continue starting up the apphost. We want to warn - // when this happens so we know we are hitting this corner case. - interactionService.DisplayMessage( - "warning", - "The HTTPS developer certificate is partially trusted. Some clients may not work correctly."); - return; - } + var outputLines = ensureCertificateCollector.GetLines(); + // Exitcode 4 means that the certs were not trusted, but there is a condition where they + // are partially trusted which we will treat as not fatal, but we will show a warning for + // diagnostic purposes just in case it is a problem for the user. + if (trustExitCode == 4 && outputLines.Any(line => line.Line == DevCertsPartialTrustMessage)) + { + interactionService.DisplayMessage( + "warning", + "The HTTPS developer certificate is partially trusted. Some clients may not work correctly."); + return; + } + else if (trustExitCode != 0) + { interactionService.DisplayLines(ensureCertificateCollector.GetLines()); throw new CertificateServiceException($"Failed to trust certificates, trust command failed with exit code: {trustExitCode}"); } From b7b080d66cad3c7c1b076e5d619ad92085f8a7f3 Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Mon, 19 May 2025 06:13:07 +0000 Subject: [PATCH 4/4] Ignore failures in certificate trust but display a warning. --- .../Certificates/CertificateService.cs | 17 ++--------- .../Certificates/CertificateServiceTests.cs | 29 +------------------ 2 files changed, 4 insertions(+), 42 deletions(-) diff --git a/src/Aspire.Cli/Certificates/CertificateService.cs b/src/Aspire.Cli/Certificates/CertificateService.cs index d8588f36b15..9dca1f534ae 100644 --- a/src/Aspire.Cli/Certificates/CertificateService.cs +++ b/src/Aspire.Cli/Certificates/CertificateService.cs @@ -42,28 +42,17 @@ public async Task EnsureCertificatesTrustedAsync(IDotNetCliRunner runner, Cancel StandardOutputCallback = ensureCertificateCollector.AppendOutput, StandardErrorCallback = ensureCertificateCollector.AppendError, }; + var trustExitCode = await interactionService.ShowStatusAsync( ":locked_with_key: Trusting certificates...", () => runner.TrustHttpCertificateAsync( options, cancellationToken)); - var outputLines = ensureCertificateCollector.GetLines(); - - // Exitcode 4 means that the certs were not trusted, but there is a condition where they - // are partially trusted which we will treat as not fatal, but we will show a warning for - // diagnostic purposes just in case it is a problem for the user. - if (trustExitCode == 4 && outputLines.Any(line => line.Line == DevCertsPartialTrustMessage)) - { - interactionService.DisplayMessage( - "warning", - "The HTTPS developer certificate is partially trusted. Some clients may not work correctly."); - return; - } - else if (trustExitCode != 0) + if (trustExitCode != 0) { interactionService.DisplayLines(ensureCertificateCollector.GetLines()); - throw new CertificateServiceException($"Failed to trust certificates, trust command failed with exit code: {trustExitCode}"); + interactionService.DisplayMessage("warning", $"Developer certificates may not be fully trusted (trust exit code was: {trustExitCode})"); } } } diff --git a/tests/Aspire.Cli.Tests/Certificates/CertificateServiceTests.cs b/tests/Aspire.Cli.Tests/Certificates/CertificateServiceTests.cs index a728f10b212..7aa4b25f495 100644 --- a/tests/Aspire.Cli.Tests/Certificates/CertificateServiceTests.cs +++ b/tests/Aspire.Cli.Tests/Certificates/CertificateServiceTests.cs @@ -12,7 +12,7 @@ namespace Aspire.Cli.Tests.Certificates; public class CertificateServiceTests(ITestOutputHelper outputHelper) { [Fact] - public async Task EnsureCertificatesTrustedAsyncSucceedsOnExitCode4IfPartialTrustMessageDetected() + public async Task EnsureCertificatesTrustedAsyncSucceedsOnNonZeroExitCode() { var services = CliTestHelper.CreateServiceCollection(outputHelper, options => { @@ -37,31 +37,4 @@ public async Task EnsureCertificatesTrustedAsyncSucceedsOnExitCode4IfPartialTrus // If this does not throw then the code is behaving correctly. await cs.EnsureCertificatesTrustedAsync(runner, TestContext.Current.CancellationToken).WaitAsync(CliTestConstants.DefaultTimeout); } - - [Fact] - public async Task EnsureCertificatesTrustedAsyncFailsOnExitCode4IfPartialTrustMessageNotDetected() - { - var services = CliTestHelper.CreateServiceCollection(outputHelper, options => - { - options.DotNetCliRunnerFactory = sp => - { - var runner = new TestDotNetCliRunner(); - runner.CheckHttpCertificateAsyncCallback = (_, _) => 1; - runner.TrustHttpCertificateAsyncCallback = (options, _) => 4; - return runner; - }; - }); - - var sp = services.BuildServiceProvider(); - var cs = sp.GetRequiredService(); - var runner = sp.GetRequiredService(); - - var ex = await Assert.ThrowsAsync(async () => - { - await cs.EnsureCertificatesTrustedAsync(runner, TestContext.Current.CancellationToken).WaitAsync(CliTestConstants.DefaultTimeout); - - }); - - Assert.Equal("Failed to trust certificates, trust command failed with exit code: 4", ex.Message); - } } \ No newline at end of file