diff --git a/src/Aspire.Hosting/AspireEventSource.cs b/src/Aspire.Hosting/AspireEventSource.cs index cdea12bded7..9ff1b6a6c3e 100644 --- a/src/Aspire.Hosting/AspireEventSource.cs +++ b/src/Aspire.Hosting/AspireEventSource.cs @@ -370,4 +370,22 @@ public void StartResourceStop(string kind, string resourceName) WriteEvent(42, kind, resourceName); } } + + [Event(43, Level = EventLevel.Informational, Message = "Development certificate trust check is starting...")] + public void DevelopmentCertificateTrustCheckStart() + { + if (IsEnabled()) + { + WriteEvent(43); + } + } + + [Event(44, Level = EventLevel.Informational, Message = "Development certificate trust check completed")] + public void DevelopmentCertificateTrustCheckStop() + { + if (IsEnabled()) + { + WriteEvent(44); + } + } } diff --git a/src/Aspire.Hosting/Dashboard/DashboardEventHandlers.cs b/src/Aspire.Hosting/Dashboard/DashboardEventHandlers.cs index 9a559e79d35..929144e9fd8 100644 --- a/src/Aspire.Hosting/Dashboard/DashboardEventHandlers.cs +++ b/src/Aspire.Hosting/Dashboard/DashboardEventHandlers.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. #pragma warning disable ASPIREFILESYSTEM001 // Type is for evaluation purposes only +#pragma warning disable ASPIRECERTIFICATES001 // Type is for evaluation purposes only using System.Collections.Concurrent; using System.Diagnostics; @@ -412,6 +413,7 @@ private void ConfigureAspireDashboardResource(IResource dashboardResource) if (otlpGrpcEndpointUrl != null) { var address = BindingAddress.Parse(otlpGrpcEndpointUrl); + dashboardResource.Annotations.Add(new EndpointAnnotation(ProtocolType.Tcp, name: OtlpGrpcEndpointName, uriScheme: address.Scheme, port: address.Port, isProxied: true, transport: "http2") { TargetHost = address.Host @@ -421,6 +423,7 @@ private void ConfigureAspireDashboardResource(IResource dashboardResource) if (otlpHttpEndpointUrl != null) { var address = BindingAddress.Parse(otlpHttpEndpointUrl); + dashboardResource.Annotations.Add(new EndpointAnnotation(ProtocolType.Tcp, name: OtlpHttpEndpointName, uriScheme: address.Scheme, port: address.Port, isProxied: true) { TargetHost = address.Host @@ -430,16 +433,46 @@ private void ConfigureAspireDashboardResource(IResource dashboardResource) if (mcpEndpointUrl != null) { var address = BindingAddress.Parse(mcpEndpointUrl); + dashboardResource.Annotations.Add(new EndpointAnnotation(ProtocolType.Tcp, name: McpEndpointName, uriScheme: address.Scheme, port: address.Port, isProxied: true) { TargetHost = address.Host }); } + // Determine whether any HTTPS endpoints are configured + var hasHttpsEndpoint = dashboardResource.TryGetAnnotationsOfType(out var endpoints) && endpoints.Any(e => e.UriScheme is "https"); + + if (hasHttpsEndpoint && + !dashboardResource.HasAnnotationOfType()) + { + var developerCertificateService = executionContext.ServiceProvider.GetRequiredService(); + var trustDeveloperCertificate = developerCertificateService.TrustCertificate; + if (dashboardResource.TryGetLastAnnotation(out var certificateAuthorityAnnotation)) + { + trustDeveloperCertificate = certificateAuthorityAnnotation.TrustDeveloperCertificates.GetValueOrDefault(trustDeveloperCertificate); + } + + if (trustDeveloperCertificate) + { + dashboardResource.Annotations.Add(new HttpsCertificateConfigurationCallbackAnnotation(ctx => + { + ctx.EnvironmentVariables["Kestrel__Certificates__Default__Path"] = ctx.CertificatePath; + ctx.EnvironmentVariables["Kestrel__Certificates__Default__KeyPath"] = ctx.KeyPath; + if (ctx.Password is not null) + { + ctx.EnvironmentVariables["Kestrel__Certificates__Default__Password"] = ctx.Password; + } + + return Task.CompletedTask; + })); + } + } + dashboardResource.Annotations.Add(new ResourceUrlsCallbackAnnotation(c => { var browserToken = options.DashboardToken; - + foreach (var url in c.Urls) { if (url.Endpoint is { } endpoint) @@ -450,7 +483,7 @@ private void ConfigureAspireDashboardResource(IResource dashboardResource) // Order these before non-browser usable endpoints. url.DisplayText = $"Dashboard ({endpoint.EndpointName})"; url.DisplayOrder = 1; - + // Append the browser token to the URL as a query string parameter if token is configured if (!string.IsNullOrEmpty(browserToken)) { diff --git a/src/Aspire.Hosting/Dcp/DcpHost.cs b/src/Aspire.Hosting/Dcp/DcpHost.cs index bb8ca80ce29..ecbd9af7c2d 100644 --- a/src/Aspire.Hosting/Dcp/DcpHost.cs +++ b/src/Aspire.Hosting/Dcp/DcpHost.cs @@ -3,6 +3,7 @@ using System.Buffers; using System.Collections; +using System.Globalization; using System.IO.Pipelines; using System.Net.Sockets; using System.Text; @@ -10,12 +11,15 @@ using Aspire.Hosting.ApplicationModel; using Aspire.Hosting.Dcp.Process; using Aspire.Hosting.Resources; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; namespace Aspire.Hosting.Dcp; #pragma warning disable ASPIREINTERACTION001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. +#pragma warning disable ASPIRECERTIFICATES001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. +#pragma warning disable ASPIREFILESYSTEM001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. internal sealed class DcpHost { @@ -29,6 +33,9 @@ internal sealed class DcpHost private readonly IInteractionService _interactionService; private readonly Locations _locations; private readonly TimeProvider _timeProvider; + private readonly IDeveloperCertificateService _developerCertificateService; + private readonly IFileSystemService _fileSystemService; + private readonly IConfiguration _configuration; private readonly CancellationTokenSource _shutdownCts = new(); private Task? _logProcessorTask; @@ -48,7 +55,10 @@ public DcpHost( IInteractionService interactionService, Locations locations, DistributedApplicationModel applicationModel, - TimeProvider timeProvider) + TimeProvider timeProvider, + IDeveloperCertificateService developerCertificateService, + IFileSystemService fileSystemService, + IConfiguration configuration) { _loggerFactory = loggerFactory; _logger = loggerFactory.CreateLogger(); @@ -58,11 +68,15 @@ public DcpHost( _locations = locations; _applicationModel = applicationModel; _timeProvider = timeProvider; + _developerCertificateService = developerCertificateService; + _fileSystemService = fileSystemService; + _configuration = configuration; } public async Task StartAsync(CancellationToken cancellationToken) { await EnsureDcpContainerRuntimeAsync(cancellationToken).ConfigureAwait(false); + await EnsureDevelopmentCertificateTrustAsync(cancellationToken).ConfigureAwait(false); EnsureDcpHostRunning(); } @@ -111,7 +125,7 @@ internal async Task EnsureDcpContainerRuntimeAsync(CancellationToken cancellatio if (dcpInfo is not null) { DcpDependencyCheck.CheckDcpInfoAndLogErrors(_logger, _dcpOptions, dcpInfo, throwIfUnhealthy: requireContainerRuntimeInitialization); - + // Show UI notification if container runtime is unhealthy TryShowContainerRuntimeNotification(dcpInfo, cancellationToken); } @@ -122,6 +136,50 @@ internal async Task EnsureDcpContainerRuntimeAsync(CancellationToken cancellatio } } + internal async Task EnsureDevelopmentCertificateTrustAsync(CancellationToken cancellationToken) + { + AspireEventSource.Instance.DevelopmentCertificateTrustCheckStart(); + + try + { + // Check if the interaction service is available (dashboard enabled) + if (!_interactionService.IsAvailable) + { + return; + } + + // Check and warn if the developer certificate is not trusted + if (_developerCertificateService.TrustCertificate && _developerCertificateService.Certificates.Count > 0 && !await DeveloperCertificateService.IsCertificateTrustedAsync(_fileSystemService, _developerCertificateService.Certificates.First(), cancellationToken).ConfigureAwait(false)) + { + var trustLocation = "your project folder"; + var appHostDirectory = _configuration["AppHost:Directory"]; + if (!string.IsNullOrWhiteSpace(appHostDirectory)) + { + trustLocation = $"'{appHostDirectory}'"; + } + + var title = InteractionStrings.DeveloperCertificateNotFullyTrustedTitle; + var message = string.Format(CultureInfo.CurrentCulture, InteractionStrings.DeveloperCertificateNotFullyTrustedMessage, trustLocation); + + _logger.LogWarning("{Message}", message); + + // Send notification to the dashboard + _ = _interactionService.PromptNotificationAsync( + title: title, + message: message, + options: new NotificationInteractionOptions + { + Intent = MessageIntent.Error, + }, + cancellationToken: cancellationToken); + } + } + finally + { + AspireEventSource.Instance.DevelopmentCertificateTrustCheckStop(); + } + } + public async Task StopAsync() { _shutdownCts.Cancel(); @@ -408,7 +466,7 @@ private void TryShowContainerRuntimeNotification(DcpInfo dcpInfo, CancellationTo // Create a cancellation token source that can be cancelled when runtime becomes healthy var notificationCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _shutdownCts.Token); - + // Single background task to show notification and poll for health updates _ = Task.Run(async () => { @@ -424,7 +482,7 @@ private void TryShowContainerRuntimeNotification(DcpInfo dcpInfo, CancellationTo try { var dcpInfo = await _dependencyCheckService.GetDcpInfoAsync(force: true, cancellationToken: notificationCts.Token).ConfigureAwait(false); - + if (dcpInfo is not null && IsContainerRuntimeHealthy(dcpInfo)) { // Container runtime is now healthy, exit the polling loop @@ -442,10 +500,10 @@ private void TryShowContainerRuntimeNotification(DcpInfo dcpInfo, CancellationTo _logger.LogDebug(ex, "Error while polling container runtime health for notification"); } } - + // Cancel the notification at the end of the loop notificationCts.Cancel(); - + // Wait for notification task to complete try { diff --git a/src/Aspire.Hosting/DeveloperCertificateService.cs b/src/Aspire.Hosting/DeveloperCertificateService.cs index bb8a570272a..b9c2078e2bd 100644 --- a/src/Aspire.Hosting/DeveloperCertificateService.cs +++ b/src/Aspire.Hosting/DeveloperCertificateService.cs @@ -1,8 +1,10 @@ #pragma warning disable ASPIRECERTIFICATES001 +#pragma warning disable ASPIREFILESYSTEM001 // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Aspire.Hosting.Dcp.Process; using Aspire.Hosting.Utils; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; @@ -19,6 +21,10 @@ internal class DeveloperCertificateService : IDeveloperCertificateService public DeveloperCertificateService(ILogger logger, IConfiguration configuration, DistributedApplicationOptions options) { + TrustCertificate = configuration.GetBool(KnownConfigNames.DeveloperCertificateDefaultTrust) ?? + options.TrustDeveloperCertificate ?? + true; + _certificates = new Lazy>(() => { try @@ -33,19 +39,19 @@ public DeveloperCertificateService(ILogger logger, // so we want to ensure the certificate that will be used by ASP.NET Core is the first one in the bundle. // Match the ordering logic ASP.NET Core uses, including DateTimeOffset.Now for current time: https://github.com/dotnet/aspnetcore/blob/0aefdae365ff9b73b52961acafd227309524ce3c/src/Shared/CertificateGeneration/CertificateManager.cs#L122 var now = DateTimeOffset.Now; - + // Get all valid ASP.NET Core development certificates var validCerts = store.Certificates .Where(c => c.IsAspNetCoreDevelopmentCertificate()) .Where(c => c.NotBefore <= now && now <= c.NotAfter) .ToList(); - + // If any certificate has a Subject Key Identifier extension, exclude certificates without it if (validCerts.Any(c => c.HasSubjectKeyIdentifier())) { validCerts = validCerts.Where(c => c.HasSubjectKeyIdentifier()).ToList(); } - + // Take the highest version valid certificate for each unique SKI devCerts.AddRange( validCerts @@ -53,6 +59,12 @@ public DeveloperCertificateService(ILogger logger, .SelectMany(g => g.OrderByDescending(c => c.GetCertificateVersion()).ThenByDescending(c => c.NotAfter).Take(1)) .OrderByDescending(c => c.GetCertificateVersion()).ThenByDescending(c => c.NotAfter)); + // Release the unused certificates + foreach (var unusedCert in validCerts.Except(devCerts)) + { + unusedCert.Dispose(); + } + if (devCerts.Count == 0) { logger.LogInformation("No ASP.NET Core developer certificates found in the CurrentUser/My certificate store."); @@ -82,15 +94,10 @@ public DeveloperCertificateService(ILogger logger, return supportsTlsTermination; }); - // Environment variable config > DistributedApplicationOptions > default true - TrustCertificate = configuration.GetBool(KnownConfigNames.DeveloperCertificateDefaultTrust) ?? - options.TrustDeveloperCertificate ?? - true; - // By default, only use for server authentication if trust is also enabled (and a developer certificate with a private key is available) UseForHttps = (configuration.GetBool(KnownConfigNames.DeveloperCertificateDefaultHttpsTermination) ?? options.DeveloperCertificateDefaultHttpsTerminationEnabled ?? - true ) && TrustCertificate && _supportsTlsTermination.Value; + true) && TrustCertificate && _supportsTlsTermination.Value; } /// @@ -104,4 +111,71 @@ public DeveloperCertificateService(ILogger logger, /// public bool UseForHttps { get; } + + internal static async Task IsCertificateTrustedAsync(IFileSystemService fileSystemService, X509Certificate2 certificate, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(certificate); + + if (OperatingSystem.IsMacOS()) + { + // On MacOS we have to verify against the Keychain + return await IsCertificateTrustedInMacOsKeychainAsync(fileSystemService, certificate, cancellationToken).ConfigureAwait(false); + } + + try + { + // On Linux and Windows, we need to check if the certificate is in the Root store + using var store = new X509Store(StoreName.Root, StoreLocation.CurrentUser); + store.Open(OpenFlags.ReadOnly); + + var matches = store.Certificates.Find(X509FindType.FindByThumbprint, certificate.Thumbprint, validOnly: false); + try + { + return matches.Count > 0; + } + finally + { + foreach (var cert in matches) + { + cert.Dispose(); + } + } + } + catch + { + // Ignore errors and assume not trusted + return false; + } + } + + // Use the same approach as `dotnet dev-certs` to check if the certificate is trusted in the macOS keychain + // See: https://github.com/dotnet/aspnetcore/blob/2a88012113497bac5056548f16d810738b069198/src/Shared/CertificateGeneration/MacOSCertificateManager.cs#L36-L37 + private static async Task IsCertificateTrustedInMacOsKeychainAsync(IFileSystemService fileSystemService, X509Certificate2 certificate, CancellationToken cancellationToken) + { + try + { + using var tempDirectory = fileSystemService.TempDirectory.CreateTempSubdirectory("aspire-devcert-check"); + var certPath = Path.Combine(tempDirectory.Path, $"aspire-devcert-{certificate.Thumbprint}.cer"); + + File.WriteAllBytes(certPath, certificate.Export(X509ContentType.Cert)); + + var processSpec = new ProcessSpec("security") + { + Arguments = $"verify-cert -p basic -p ssl -c {certPath}", + ThrowOnNonZeroReturnCode = false + }; + + var (task, processDisposable) = ProcessUtil.Run(processSpec); + await using (processDisposable.ConfigureAwait(false)) + { + var result = await task.WaitAsync(cancellationToken).ConfigureAwait(false); + return result.ExitCode == 0; + } + } + catch + { + // Ignore errors and assume not trusted + return false; + } + } } diff --git a/src/Aspire.Hosting/Resources/InteractionStrings.Designer.cs b/src/Aspire.Hosting/Resources/InteractionStrings.Designer.cs index b04ef4bab94..4c733eba634 100644 --- a/src/Aspire.Hosting/Resources/InteractionStrings.Designer.cs +++ b/src/Aspire.Hosting/Resources/InteractionStrings.Designer.cs @@ -10,8 +10,8 @@ namespace Aspire.Hosting.Resources { using System; - - + + /// /// A strongly-typed resource class, for looking up localized strings, etc. /// @@ -23,15 +23,15 @@ namespace Aspire.Hosting.Resources { [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class InteractionStrings { - + private static global::System.Resources.ResourceManager resourceMan; - + private static global::System.Globalization.CultureInfo resourceCulture; - + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] internal InteractionStrings() { } - + /// /// Returns the cached ResourceManager instance used by this class. /// @@ -45,7 +45,7 @@ internal InteractionStrings() { return resourceMan; } } - + /// /// Overrides the current thread's CurrentUICulture property for all /// resource lookups using this strongly typed resource class. @@ -59,7 +59,7 @@ internal InteractionStrings() { resourceCulture = value; } } - + /// /// Looks up a localized string similar to Ensure that Docker is running and that the Docker daemon is accessible. If Resource Saver mode is enabled, containers may not run.. /// @@ -68,7 +68,7 @@ internal static string ContainerRuntimeDockerAdvice { return ResourceManager.GetString("ContainerRuntimeDockerAdvice", resourceCulture); } } - + /// /// Looks up a localized string similar to Ensure that the container runtime is running.. /// @@ -77,7 +77,7 @@ internal static string ContainerRuntimeGenericAdvice { return ResourceManager.GetString("ContainerRuntimeGenericAdvice", resourceCulture); } } - + /// /// Looks up a localized string similar to Learn more. /// @@ -86,7 +86,7 @@ internal static string ContainerRuntimeLinkText { return ResourceManager.GetString("ContainerRuntimeLinkText", resourceCulture); } } - + /// /// Looks up a localized string similar to Container runtime could not be found. See https://aka.ms/dotnet/aspire/containers for more details on supported container runtimes.. /// @@ -95,7 +95,7 @@ internal static string ContainerRuntimeNotInstalledMessage { return ResourceManager.GetString("ContainerRuntimeNotInstalledMessage", resourceCulture); } } - + /// /// Looks up a localized string similar to Container runtime not installed. /// @@ -104,7 +104,25 @@ internal static string ContainerRuntimeNotInstalledTitle { return ResourceManager.GetString("ContainerRuntimeNotInstalledTitle", resourceCulture); } } - + + /// + /// Looks up a localized string similar to Development certificate not fully trusted. + /// + internal static string DeveloperCertificateNotFullyTrustedTitle { + get { + return ResourceManager.GetString("DeveloperCertificateNotFullyTrustedTitle", resourceCulture); + } + } + + /// + /// The most recent ASP.NET Core Development Certificate isn't fully trusted. Run `dotnet dev-certs https --trust` from {0} to trust the certificate.. + /// + internal static string DeveloperCertificateNotFullyTrustedMessage { + get { + return ResourceManager.GetString("DeveloperCertificateNotFullyTrustedMessage", resourceCulture); + } + } + /// /// Looks up a localized string similar to Ensure that Podman is running.. /// @@ -113,7 +131,7 @@ internal static string ContainerRuntimePodmanAdvice { return ResourceManager.GetString("ContainerRuntimePodmanAdvice", resourceCulture); } } - + /// /// Looks up a localized string similar to Container runtime '{0}' was found but appears to be unhealthy. . /// @@ -122,7 +140,7 @@ internal static string ContainerRuntimeUnhealthyMessage { return ResourceManager.GetString("ContainerRuntimeUnhealthyMessage", resourceCulture); } } - + /// /// Looks up a localized string similar to Container runtime unhealthy. /// @@ -131,7 +149,7 @@ internal static string ContainerRuntimeUnhealthyTitle { return ResourceManager.GetString("ContainerRuntimeUnhealthyTitle", resourceCulture); } } - + /// /// Looks up a localized string similar to Are you sure you want to delete the value for parameter `{0}`? . /// @@ -140,7 +158,7 @@ internal static string DeleteParameterMessage { return ResourceManager.GetString("DeleteParameterMessage", resourceCulture); } } - + /// /// Looks up a localized string similar to Are you sure you want to delete the value for parameter `{0}`? /// @@ -151,7 +169,7 @@ internal static string DeleteParameterMessageWithUserSecrets { return ResourceManager.GetString("DeleteParameterMessageWithUserSecrets", resourceCulture); } } - + /// /// Looks up a localized string similar to Delete. /// @@ -160,7 +178,7 @@ internal static string DeleteParameterPrimaryButtonText { return ResourceManager.GetString("DeleteParameterPrimaryButtonText", resourceCulture); } } - + /// /// Looks up a localized string similar to Delete parameter value. /// @@ -169,7 +187,7 @@ internal static string DeleteParameterTitle { return ResourceManager.GetString("DeleteParameterTitle", resourceCulture); } } - + /// /// Looks up a localized string similar to There are unresolved parameters that need to be set. Please provide values for them.. /// @@ -178,7 +196,7 @@ internal static string ParametersBarMessage { return ResourceManager.GetString("ParametersBarMessage", resourceCulture); } } - + /// /// Looks up a localized string similar to Enter values. /// @@ -187,7 +205,7 @@ internal static string ParametersBarPrimaryButtonText { return ResourceManager.GetString("ParametersBarPrimaryButtonText", resourceCulture); } } - + /// /// Looks up a localized string similar to Unresolved parameters. /// @@ -196,7 +214,7 @@ internal static string ParametersBarTitle { return ResourceManager.GetString("ParametersBarTitle", resourceCulture); } } - + /// /// Looks up a localized string similar to Delete from user secrets. /// @@ -205,7 +223,7 @@ internal static string ParametersInputsDeleteLabel { return ResourceManager.GetString("ParametersInputsDeleteLabel", resourceCulture); } } - + /// /// Looks up a localized string similar to Please provide values for the unresolved parameters. Parameters can be saved to [user secrets](https://aka.ms/aspire/user-secrets) for future use.. /// @@ -214,7 +232,7 @@ internal static string ParametersInputsMessage { return ResourceManager.GetString("ParametersInputsMessage", resourceCulture); } } - + /// /// Looks up a localized string similar to Please provide values for the unresolved parameters.. /// @@ -223,7 +241,7 @@ internal static string ParametersInputsMessagePublishMode { return ResourceManager.GetString("ParametersInputsMessagePublishMode", resourceCulture); } } - + /// /// Looks up a localized string similar to Enter value for {0}. /// @@ -232,7 +250,7 @@ internal static string ParametersInputsParameterPlaceholder { return ResourceManager.GetString("ParametersInputsParameterPlaceholder", resourceCulture); } } - + /// /// Looks up a localized string similar to Save. /// @@ -241,7 +259,7 @@ internal static string ParametersInputsPrimaryButtonText { return ResourceManager.GetString("ParametersInputsPrimaryButtonText", resourceCulture); } } - + /// /// Looks up a localized string similar to Save to user secrets. /// @@ -250,7 +268,7 @@ internal static string ParametersInputsRememberLabel { return ResourceManager.GetString("ParametersInputsRememberLabel", resourceCulture); } } - + /// /// Looks up a localized string similar to Requires a `UserSecretsId` to be configured in the AppHost project file. Run `dotnet user-secrets init` in the AppHost directory to configure.. /// @@ -259,7 +277,7 @@ internal static string ParametersInputsRememberDescriptionNotConfigured { return ResourceManager.GetString("ParametersInputsRememberDescriptionNotConfigured", resourceCulture); } } - + /// /// Looks up a localized string similar to Set unresolved parameters. /// @@ -268,7 +286,7 @@ internal static string ParametersInputsTitle { return ResourceManager.GetString("ParametersInputsTitle", resourceCulture); } } - + /// /// Looks up a localized string similar to Please provide a value for the parameter. The parameter can be saved to [user secrets](https://learn.microsoft.com/aspnet/core/security/app-secrets) for future use. /// @@ -279,7 +297,7 @@ internal static string SetParameterMessage { return ResourceManager.GetString("SetParameterMessage", resourceCulture); } } - + /// /// Looks up a localized string similar to Set parameter. /// @@ -288,7 +306,7 @@ internal static string SetParameterTitle { return ResourceManager.GetString("SetParameterTitle", resourceCulture); } } - + /// /// Looks up a localized string similar to Upgrade instructions. /// @@ -297,7 +315,7 @@ internal static string VersionCheckLinkText { return ResourceManager.GetString("VersionCheckLinkText", resourceCulture); } } - + /// /// Looks up a localized string similar to Aspire {0} is available.. /// @@ -306,7 +324,7 @@ internal static string VersionCheckMessage { return ResourceManager.GetString("VersionCheckMessage", resourceCulture); } } - + /// /// Looks up a localized string similar to Ignore. /// @@ -315,7 +333,7 @@ internal static string VersionCheckPrimaryButtonText { return ResourceManager.GetString("VersionCheckPrimaryButtonText", resourceCulture); } } - + /// /// Looks up a localized string similar to Update now. /// diff --git a/src/Aspire.Hosting/Resources/InteractionStrings.resx b/src/Aspire.Hosting/Resources/InteractionStrings.resx index 1c945400c4a..a06681257f4 100644 --- a/src/Aspire.Hosting/Resources/InteractionStrings.resx +++ b/src/Aspire.Hosting/Resources/InteractionStrings.resx @@ -1,17 +1,17 @@ - @@ -184,6 +184,12 @@ Container runtime could not be found. See https://aka.ms/dotnet/aspire/containers for more details on supported container runtimes. + + Development certificate not fully trusted + + + The most recent ASP.NET Core Development Certificate isn't fully trusted. Run `dotnet dev-certs https --trust` from {0} to trust the certificate. + Set parameter diff --git a/src/Aspire.Hosting/Resources/xlf/InteractionStrings.cs.xlf b/src/Aspire.Hosting/Resources/xlf/InteractionStrings.cs.xlf index 2fe268b765f..1975a22a6ca 100644 --- a/src/Aspire.Hosting/Resources/xlf/InteractionStrings.cs.xlf +++ b/src/Aspire.Hosting/Resources/xlf/InteractionStrings.cs.xlf @@ -66,6 +66,16 @@ Hodnota je aktuálně uložena v [tajných klíčích uživatele](https://learn. Odstranit hodnotu parametru + + The most recent ASP.NET Core Development Certificate isn't fully trusted. Run `dotnet dev-certs https --trust` from {0} to trust the certificate. + The most recent ASP.NET Core Development Certificate isn't fully trusted. Run `dotnet dev-certs https --trust` from {0} to trust the certificate. + + + + Development certificate not fully trusted + Development certificate not fully trusted + + There are unresolved parameters that need to be set. Please provide values for them. Existují nevyřešené parametry, které je třeba nastavit. Zadejte pro ně hodnoty. diff --git a/src/Aspire.Hosting/Resources/xlf/InteractionStrings.de.xlf b/src/Aspire.Hosting/Resources/xlf/InteractionStrings.de.xlf index e7ffd85fa8a..b0f00d9504d 100644 --- a/src/Aspire.Hosting/Resources/xlf/InteractionStrings.de.xlf +++ b/src/Aspire.Hosting/Resources/xlf/InteractionStrings.de.xlf @@ -66,6 +66,16 @@ Der Wert wird derzeit in [Benutzergeheimnissen](https://learn.microsoft.com/aspn Parameterwert löschen + + The most recent ASP.NET Core Development Certificate isn't fully trusted. Run `dotnet dev-certs https --trust` from {0} to trust the certificate. + The most recent ASP.NET Core Development Certificate isn't fully trusted. Run `dotnet dev-certs https --trust` from {0} to trust the certificate. + + + + Development certificate not fully trusted + Development certificate not fully trusted + + There are unresolved parameters that need to be set. Please provide values for them. Es gibt nicht aufgelöste Parameter, die festgelegt werden müssen. Geben Sie Werte für diese an. diff --git a/src/Aspire.Hosting/Resources/xlf/InteractionStrings.es.xlf b/src/Aspire.Hosting/Resources/xlf/InteractionStrings.es.xlf index d2f8914a374..cd31e0d2d44 100644 --- a/src/Aspire.Hosting/Resources/xlf/InteractionStrings.es.xlf +++ b/src/Aspire.Hosting/Resources/xlf/InteractionStrings.es.xlf @@ -66,6 +66,16 @@ El valor se guarda actualmente en [secretos de usuario](https://learn.microsoft. Eliminar valor de parámetro + + The most recent ASP.NET Core Development Certificate isn't fully trusted. Run `dotnet dev-certs https --trust` from {0} to trust the certificate. + The most recent ASP.NET Core Development Certificate isn't fully trusted. Run `dotnet dev-certs https --trust` from {0} to trust the certificate. + + + + Development certificate not fully trusted + Development certificate not fully trusted + + There are unresolved parameters that need to be set. Please provide values for them. Hay parámetros sin resolver que deben configurarse. Proporcione valores para ellos. diff --git a/src/Aspire.Hosting/Resources/xlf/InteractionStrings.fr.xlf b/src/Aspire.Hosting/Resources/xlf/InteractionStrings.fr.xlf index cc0ef874bc5..30d4bf04304 100644 --- a/src/Aspire.Hosting/Resources/xlf/InteractionStrings.fr.xlf +++ b/src/Aspire.Hosting/Resources/xlf/InteractionStrings.fr.xlf @@ -66,6 +66,16 @@ La valeur est actuellement enregistrée dans [secrets utilisateur](https://learn Supprimer la valeur du paramètre + + The most recent ASP.NET Core Development Certificate isn't fully trusted. Run `dotnet dev-certs https --trust` from {0} to trust the certificate. + The most recent ASP.NET Core Development Certificate isn't fully trusted. Run `dotnet dev-certs https --trust` from {0} to trust the certificate. + + + + Development certificate not fully trusted + Development certificate not fully trusted + + There are unresolved parameters that need to be set. Please provide values for them. Vous devez définir des paramètres non résolus. Veuillez leur fournir des valeurs. diff --git a/src/Aspire.Hosting/Resources/xlf/InteractionStrings.it.xlf b/src/Aspire.Hosting/Resources/xlf/InteractionStrings.it.xlf index 69f9697134e..527b6c3d3dd 100644 --- a/src/Aspire.Hosting/Resources/xlf/InteractionStrings.it.xlf +++ b/src/Aspire.Hosting/Resources/xlf/InteractionStrings.it.xlf @@ -66,6 +66,16 @@ Il valore è attualmente salvato nei [segreti utente](https://learn.microsoft.co Elimina valore parametro + + The most recent ASP.NET Core Development Certificate isn't fully trusted. Run `dotnet dev-certs https --trust` from {0} to trust the certificate. + The most recent ASP.NET Core Development Certificate isn't fully trusted. Run `dotnet dev-certs https --trust` from {0} to trust the certificate. + + + + Development certificate not fully trusted + Development certificate not fully trusted + + There are unresolved parameters that need to be set. Please provide values for them. Sono presenti parametri non risolti che devono essere impostati. Specifica i valori per questi parametri. diff --git a/src/Aspire.Hosting/Resources/xlf/InteractionStrings.ja.xlf b/src/Aspire.Hosting/Resources/xlf/InteractionStrings.ja.xlf index 3b12632817e..c0ecbcb6db0 100644 --- a/src/Aspire.Hosting/Resources/xlf/InteractionStrings.ja.xlf +++ b/src/Aspire.Hosting/Resources/xlf/InteractionStrings.ja.xlf @@ -66,6 +66,16 @@ The value is currently saved in [user secrets](https://learn.microsoft.com/aspne パラメーター値の削除 + + The most recent ASP.NET Core Development Certificate isn't fully trusted. Run `dotnet dev-certs https --trust` from {0} to trust the certificate. + The most recent ASP.NET Core Development Certificate isn't fully trusted. Run `dotnet dev-certs https --trust` from {0} to trust the certificate. + + + + Development certificate not fully trusted + Development certificate not fully trusted + + There are unresolved parameters that need to be set. Please provide values for them. 設定する必要のある未解決のパラメーターがあります。それらの値を指定してください。 diff --git a/src/Aspire.Hosting/Resources/xlf/InteractionStrings.ko.xlf b/src/Aspire.Hosting/Resources/xlf/InteractionStrings.ko.xlf index 77659df1398..82b6f4abf46 100644 --- a/src/Aspire.Hosting/Resources/xlf/InteractionStrings.ko.xlf +++ b/src/Aspire.Hosting/Resources/xlf/InteractionStrings.ko.xlf @@ -66,6 +66,16 @@ The value is currently saved in [user secrets](https://learn.microsoft.com/aspne 매개 변수 값 삭제 + + The most recent ASP.NET Core Development Certificate isn't fully trusted. Run `dotnet dev-certs https --trust` from {0} to trust the certificate. + The most recent ASP.NET Core Development Certificate isn't fully trusted. Run `dotnet dev-certs https --trust` from {0} to trust the certificate. + + + + Development certificate not fully trusted + Development certificate not fully trusted + + There are unresolved parameters that need to be set. Please provide values for them. 설정해야 하는 해결되지 않은 매개 변수가 있습니다. 값을 입력하세요. diff --git a/src/Aspire.Hosting/Resources/xlf/InteractionStrings.pl.xlf b/src/Aspire.Hosting/Resources/xlf/InteractionStrings.pl.xlf index 5faf8c7b62b..0467fd9d266 100644 --- a/src/Aspire.Hosting/Resources/xlf/InteractionStrings.pl.xlf +++ b/src/Aspire.Hosting/Resources/xlf/InteractionStrings.pl.xlf @@ -66,6 +66,16 @@ Wartość jest obecnie zapisana we [wpisach tajnych użytkownika](https://learn. Usuń wartość parametru + + The most recent ASP.NET Core Development Certificate isn't fully trusted. Run `dotnet dev-certs https --trust` from {0} to trust the certificate. + The most recent ASP.NET Core Development Certificate isn't fully trusted. Run `dotnet dev-certs https --trust` from {0} to trust the certificate. + + + + Development certificate not fully trusted + Development certificate not fully trusted + + There are unresolved parameters that need to be set. Please provide values for them. Istnieją nierozpoznane parametry, które należy ustawić. Podaj dla nich wartości. diff --git a/src/Aspire.Hosting/Resources/xlf/InteractionStrings.pt-BR.xlf b/src/Aspire.Hosting/Resources/xlf/InteractionStrings.pt-BR.xlf index e9429a32641..000ef320731 100644 --- a/src/Aspire.Hosting/Resources/xlf/InteractionStrings.pt-BR.xlf +++ b/src/Aspire.Hosting/Resources/xlf/InteractionStrings.pt-BR.xlf @@ -66,6 +66,16 @@ O valor está atualmente salvo em [segredos do usuário](https://learn.microsof Excluir valor do parâmetro + + The most recent ASP.NET Core Development Certificate isn't fully trusted. Run `dotnet dev-certs https --trust` from {0} to trust the certificate. + The most recent ASP.NET Core Development Certificate isn't fully trusted. Run `dotnet dev-certs https --trust` from {0} to trust the certificate. + + + + Development certificate not fully trusted + Development certificate not fully trusted + + There are unresolved parameters that need to be set. Please provide values for them. Há parâmetros não resolvidos que precisam ser definidos. Insira valores para eles. diff --git a/src/Aspire.Hosting/Resources/xlf/InteractionStrings.ru.xlf b/src/Aspire.Hosting/Resources/xlf/InteractionStrings.ru.xlf index ece4ec93fd1..5ef4bc73c77 100644 --- a/src/Aspire.Hosting/Resources/xlf/InteractionStrings.ru.xlf +++ b/src/Aspire.Hosting/Resources/xlf/InteractionStrings.ru.xlf @@ -66,6 +66,16 @@ The value is currently saved in [user secrets](https://learn.microsoft.com/aspne Удалить значение параметра + + The most recent ASP.NET Core Development Certificate isn't fully trusted. Run `dotnet dev-certs https --trust` from {0} to trust the certificate. + The most recent ASP.NET Core Development Certificate isn't fully trusted. Run `dotnet dev-certs https --trust` from {0} to trust the certificate. + + + + Development certificate not fully trusted + Development certificate not fully trusted + + There are unresolved parameters that need to be set. Please provide values for them. Существуют неразрешенные параметры, которые необходимо настроить. Укажите значения для них. diff --git a/src/Aspire.Hosting/Resources/xlf/InteractionStrings.tr.xlf b/src/Aspire.Hosting/Resources/xlf/InteractionStrings.tr.xlf index 1b2aa148f7f..505ae762387 100644 --- a/src/Aspire.Hosting/Resources/xlf/InteractionStrings.tr.xlf +++ b/src/Aspire.Hosting/Resources/xlf/InteractionStrings.tr.xlf @@ -66,6 +66,16 @@ Değer şu anda [kullanıcı gizli dizinleri](https://learn.microsoft.com/aspnet Parametre değerini sil + + The most recent ASP.NET Core Development Certificate isn't fully trusted. Run `dotnet dev-certs https --trust` from {0} to trust the certificate. + The most recent ASP.NET Core Development Certificate isn't fully trusted. Run `dotnet dev-certs https --trust` from {0} to trust the certificate. + + + + Development certificate not fully trusted + Development certificate not fully trusted + + There are unresolved parameters that need to be set. Please provide values for them. Ayarlanması gereken çözülmemiş parametreler var. Lütfen bunlara değerler girin. diff --git a/src/Aspire.Hosting/Resources/xlf/InteractionStrings.zh-Hans.xlf b/src/Aspire.Hosting/Resources/xlf/InteractionStrings.zh-Hans.xlf index 53a42de7440..d0199fbb761 100644 --- a/src/Aspire.Hosting/Resources/xlf/InteractionStrings.zh-Hans.xlf +++ b/src/Aspire.Hosting/Resources/xlf/InteractionStrings.zh-Hans.xlf @@ -66,6 +66,16 @@ The value is currently saved in [user secrets](https://learn.microsoft.com/aspne 删除参数值 + + The most recent ASP.NET Core Development Certificate isn't fully trusted. Run `dotnet dev-certs https --trust` from {0} to trust the certificate. + The most recent ASP.NET Core Development Certificate isn't fully trusted. Run `dotnet dev-certs https --trust` from {0} to trust the certificate. + + + + Development certificate not fully trusted + Development certificate not fully trusted + + There are unresolved parameters that need to be set. Please provide values for them. 有未解析的参数需要设置。请为这些参数提供值。 diff --git a/src/Aspire.Hosting/Resources/xlf/InteractionStrings.zh-Hant.xlf b/src/Aspire.Hosting/Resources/xlf/InteractionStrings.zh-Hant.xlf index 8fe6f5ce074..41bb80cbfa1 100644 --- a/src/Aspire.Hosting/Resources/xlf/InteractionStrings.zh-Hant.xlf +++ b/src/Aspire.Hosting/Resources/xlf/InteractionStrings.zh-Hant.xlf @@ -66,6 +66,16 @@ The value is currently saved in [user secrets](https://learn.microsoft.com/aspne 刪除參數值 + + The most recent ASP.NET Core Development Certificate isn't fully trusted. Run `dotnet dev-certs https --trust` from {0} to trust the certificate. + The most recent ASP.NET Core Development Certificate isn't fully trusted. Run `dotnet dev-certs https --trust` from {0} to trust the certificate. + + + + Development certificate not fully trusted + Development certificate not fully trusted + + There are unresolved parameters that need to be set. Please provide values for them. 有些未解析的參數需要設定。請提供它們的值。 diff --git a/tests/Aspire.Hosting.Tests/Dashboard/DashboardResourceTests.cs b/tests/Aspire.Hosting.Tests/Dashboard/DashboardResourceTests.cs index 60f431318ea..ae381caab04 100644 --- a/tests/Aspire.Hosting.Tests/Dashboard/DashboardResourceTests.cs +++ b/tests/Aspire.Hosting.Tests/Dashboard/DashboardResourceTests.cs @@ -14,6 +14,8 @@ namespace Aspire.Hosting.Tests.Dashboard; +#pragma warning disable ASPIRECERTIFICATES001 // Type is for evaluation purposes only + public class DashboardResourceTests(ITestOutputHelper testOutputHelper) { [Theory] @@ -457,6 +459,33 @@ public async Task DashboardResource_OtlpGrpcEndpoint_CorsEnvVarNotSet(string? ex Assert.DoesNotContain(config, e => e.Key == DashboardConfigNames.DashboardOtlpCorsAllowedHeadersKeyName.EnvVarName); } + [Fact] + public async Task DashboardResource_HttpsEndpoint_ConfiguresKestrelCertificateCallback() + { + using var builder = TestDistributedApplicationBuilder.Create(options => + { + options.DisableDashboard = false; + options.TrustDeveloperCertificate = true; + }, testOutputHelper: testOutputHelper); + + builder.Configuration.Sources.Clear(); + + builder.Configuration.AddInMemoryCollection(new Dictionary + { + [KnownConfigNames.AspNetCoreUrls] = "https://localhost", + [KnownConfigNames.DashboardOtlpGrpcEndpointUrl] = "http://localhost" + }); + + using var app = builder.Build(); + + await app.ExecuteBeforeStartHooksAsync(default).DefaultTimeout(); + + var model = app.Services.GetRequiredService(); + var dashboard = Assert.Single(model.Resources, r => r.Name == "aspire-dashboard"); + + Assert.True(dashboard.HasAnnotationOfType()); + } + [Fact] public async Task DashboardIsNotAddedInPublishMode() { diff --git a/tests/Aspire.Hosting.Tests/Dcp/DcpHostNotificationTests.cs b/tests/Aspire.Hosting.Tests/Dcp/DcpHostNotificationTests.cs index e03ef806cd1..6fde98c70ba 100644 --- a/tests/Aspire.Hosting.Tests/Dcp/DcpHostNotificationTests.cs +++ b/tests/Aspire.Hosting.Tests/Dcp/DcpHostNotificationTests.cs @@ -4,6 +4,7 @@ using System.Globalization; using Aspire.Hosting.Dcp; using Aspire.Hosting.Resources; +using Aspire.Hosting.Tests.Utils; using Microsoft.AspNetCore.InternalTesting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -35,6 +36,10 @@ public void DcpHost_WithIInteractionService_CanBeConstructed() var applicationModel = new DistributedApplicationModel(new ResourceCollection()); var timeProvider = new FakeTimeProvider(); + var developerCertificateService = new TestDeveloperCertificateService([], false, false, false); + var fileSystemService = new FileSystemService(new ConfigurationBuilder().Build()); + var configuration = new ConfigurationBuilder().Build(); + // Act & Assert - should not throw var dcpHost = new DcpHost( loggerFactory, @@ -43,7 +48,10 @@ public void DcpHost_WithIInteractionService_CanBeConstructed() interactionService, locations, applicationModel, - timeProvider); + timeProvider, + developerCertificateService, + fileSystemService, + configuration); Assert.NotNull(dcpHost); } @@ -73,6 +81,9 @@ public async Task DcpHost_WithUnhealthyContainerRuntime_ShowsNotification() var interactionService = new TestInteractionService { IsAvailable = true }; var locations = CreateTestLocations(); var timeProvider = new FakeTimeProvider(); + var developerCertificateService = new TestDeveloperCertificateService([], false, false, false); + var fileSystemService = new FileSystemService(new ConfigurationBuilder().Build()); + var configuration = new ConfigurationBuilder().Build(); var dcpHost = new DcpHost( loggerFactory, @@ -81,7 +92,10 @@ public async Task DcpHost_WithUnhealthyContainerRuntime_ShowsNotification() interactionService, locations, applicationModel, - timeProvider); + timeProvider, + developerCertificateService, + fileSystemService, + configuration); // Act await dcpHost.EnsureDcpContainerRuntimeAsync(CancellationToken.None).DefaultTimeout(); @@ -122,6 +136,9 @@ public async Task DcpHost_WithHealthyContainerRuntime_DoesNotShowNotification() var interactionService = new TestInteractionService { IsAvailable = true }; var locations = CreateTestLocations(); var timeProvider = new FakeTimeProvider(); + var developerCertificateService = new TestDeveloperCertificateService([], false, false, false); + var fileSystemService = new FileSystemService(new ConfigurationBuilder().Build()); + var configuration = new ConfigurationBuilder().Build(); var dcpHost = new DcpHost( loggerFactory, @@ -130,7 +147,10 @@ public async Task DcpHost_WithHealthyContainerRuntime_DoesNotShowNotification() interactionService, locations, applicationModel, - timeProvider); + timeProvider, + developerCertificateService, + fileSystemService, + configuration); // Act await dcpHost.EnsureDcpContainerRuntimeAsync(CancellationToken.None).DefaultTimeout(); @@ -177,6 +197,9 @@ public async Task DcpHost_WithDashboardDisabled_DoesNotShowNotification() var interactionService = new TestInteractionService { IsAvailable = false }; // Dashboard disabled var locations = CreateTestLocations(); var timeProvider = new FakeTimeProvider(); + var developerCertificateService = new TestDeveloperCertificateService([], false, false, false); + var fileSystemService = new FileSystemService(new ConfigurationBuilder().Build()); + var configuration = new ConfigurationBuilder().Build(); var dcpHost = new DcpHost( loggerFactory, @@ -185,7 +208,10 @@ public async Task DcpHost_WithDashboardDisabled_DoesNotShowNotification() interactionService, locations, applicationModel, - timeProvider); + timeProvider, + developerCertificateService, + fileSystemService, + configuration); // Act await dcpHost.EnsureDcpContainerRuntimeAsync(CancellationToken.None).DefaultTimeout(); @@ -232,6 +258,9 @@ public async Task DcpHost_WithPodmanUnhealthy_ShowsCorrectMessage() var interactionService = new TestInteractionService { IsAvailable = true }; var locations = CreateTestLocations(); var timeProvider = new FakeTimeProvider(); + var developerCertificateService = new TestDeveloperCertificateService([], false, false, false); + var fileSystemService = new FileSystemService(new ConfigurationBuilder().Build()); + var configuration = new ConfigurationBuilder().Build(); var dcpHost = new DcpHost( loggerFactory, @@ -240,7 +269,10 @@ public async Task DcpHost_WithPodmanUnhealthy_ShowsCorrectMessage() interactionService, locations, applicationModel, - timeProvider); + timeProvider, + developerCertificateService, + fileSystemService, + configuration); // Act await dcpHost.EnsureDcpContainerRuntimeAsync(CancellationToken.None).DefaultTimeout(); @@ -282,6 +314,9 @@ public async Task DcpHost_WithUnhealthyContainerRuntime_NotificationCancelledWhe var interactionService = new TestInteractionService { IsAvailable = true }; var locations = CreateTestLocations(); var timeProvider = new FakeTimeProvider(); + var developerCertificateService = new TestDeveloperCertificateService([], false, false, false); + var fileSystemService = new FileSystemService(new ConfigurationBuilder().Build()); + var configuration = new ConfigurationBuilder().Build(); var dcpHost = new DcpHost( loggerFactory, @@ -290,7 +325,10 @@ public async Task DcpHost_WithUnhealthyContainerRuntime_NotificationCancelledWhe interactionService, locations, applicationModel, - timeProvider); + timeProvider, + developerCertificateService, + fileSystemService, + configuration); // Act await dcpHost.EnsureDcpContainerRuntimeAsync(CancellationToken.None).DefaultTimeout(); @@ -350,6 +388,9 @@ public async Task DcpHost_WithContainerRuntimeNotInstalled_ShowsNotificationWith var interactionService = new TestInteractionService { IsAvailable = true }; var locations = CreateTestLocations(); var timeProvider = new FakeTimeProvider(); + var developerCertificateService = new TestDeveloperCertificateService([], false, false, false); + var fileSystemService = new FileSystemService(new ConfigurationBuilder().Build()); + var configuration = new ConfigurationBuilder().Build(); var dcpHost = new DcpHost( loggerFactory, @@ -358,7 +399,10 @@ public async Task DcpHost_WithContainerRuntimeNotInstalled_ShowsNotificationWith interactionService, locations, applicationModel, - timeProvider); + timeProvider, + developerCertificateService, + fileSystemService, + configuration); // Act await dcpHost.EnsureDcpContainerRuntimeAsync(CancellationToken.None).DefaultTimeout();