File: ApplicationModel\HttpsCertificateExecutionConfigurationGatherer.cs
Web Access
Project: src\src\Aspire.Hosting\Aspire.Hosting.csproj (Aspire.Hosting)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
#pragma warning disable ASPIRECERTIFICATES001
 
using System.Security.Cryptography.X509Certificates;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
 
namespace Aspire.Hosting.ApplicationModel;
 
/// <summary>
/// A gatherer that collects the required configuration to apply HTTPS/TLS server authentication to a resource.
/// </summary>
internal class HttpsCertificateExecutionConfigurationGatherer : IResourceExecutionConfigurationGatherer
{
    private readonly Func<X509Certificate2, HttpsCertificateExecutionConfigurationContext> _configContextFactory;
 
    /// <summary>
    /// Initializes a new instance of <see cref="HttpsCertificateExecutionConfigurationGatherer"/>.
    /// </summary>
    /// <param name="configContextFactory">A factory for configuring server authentication certificate configuration properties.</param>
    public HttpsCertificateExecutionConfigurationGatherer(Func<X509Certificate2, HttpsCertificateExecutionConfigurationContext> configContextFactory)
    {
        _configContextFactory = configContextFactory;
    }
 
    /// <inheritdoc/>
    public async ValueTask GatherAsync(IResourceExecutionConfigurationGathererContext context, IResource resource, ILogger resourceLogger, DistributedApplicationExecutionContext executionContext, CancellationToken cancellationToken = default)
    {
        var effectiveAnnotation = new HttpsCertificateAnnotation();
        if (resource.TryGetLastAnnotation<HttpsCertificateAnnotation>(out var annotation))
        {
            effectiveAnnotation = annotation;
        }
 
        X509Certificate2? certificate = effectiveAnnotation.Certificate;
        if (certificate is null)
        {
            var developerCertificateService = executionContext.ServiceProvider.GetRequiredService<IDeveloperCertificateService>();
            if (effectiveAnnotation.UseDeveloperCertificate.GetValueOrDefault(developerCertificateService.UseForHttps))
            {
                certificate = developerCertificateService.Certificates.FirstOrDefault();
            }
        }
 
        if (certificate is null)
        {
            return;
        }
 
        var configurationContext = _configContextFactory(certificate);
 
        var additionalData = new HttpsCertificateExecutionConfigurationData
        {
            Certificate = certificate,
            KeyPathReference = configurationContext.KeyPath,
            PfxPathReference = configurationContext.PfxPath,
            Password = effectiveAnnotation.Password is not null ? await effectiveAnnotation.Password.GetValueAsync(cancellationToken).ConfigureAwait(false) : null,
        };
        context.AddAdditionalData(additionalData);
 
        var callbackContext = new HttpsCertificateConfigurationCallbackAnnotationContext
        {
            ExecutionContext = executionContext,
            Resource = resource,
            Arguments = context.Arguments,
            EnvironmentVariables = context.EnvironmentVariables,
            CertificatePath = configurationContext.CertificatePath,
            // Must use the metadata references to ensure proper tracking of usage
            KeyPath = additionalData.KeyPathReference,
            PfxPath = additionalData.PfxPathReference,
            Password = effectiveAnnotation.Password,
            CancellationToken = cancellationToken,
        };
 
        foreach (var callback in resource.TryGetAnnotationsOfType<HttpsCertificateConfigurationCallbackAnnotation>(out var callbacks) ? callbacks : Enumerable.Empty<HttpsCertificateConfigurationCallbackAnnotation>())
        {
            await callback.Callback(callbackContext).ConfigureAwait(false);
        }
 
    }
}
 
/// <summary>
/// Metadata for HTTPS/TLS server certificate configuration.
/// </summary>
public class HttpsCertificateExecutionConfigurationData : IResourceExecutionConfigurationData
{
    private ReferenceExpression? _keyPathReference;
    private TrackedReference? _trackedKeyPathReference;
 
    private ReferenceExpression? _pfxPathReference;
    private TrackedReference? _trackedPfxPathReference;
 
    /// <summary>
    /// The server authentication certificate for the resource, if any.
    /// </summary>
    public required X509Certificate2 Certificate { get; init; }
 
    /// <summary>
    /// Reference expression that will resolve to the path of the server authentication certificate key in PEM format.
    /// </summary>
    public required ReferenceExpression KeyPathReference
    {
        get
        {
            return _keyPathReference!;
        }
        set
        {
            _trackedKeyPathReference = new TrackedReference(value);
            _keyPathReference = ReferenceExpression.Create($"{_trackedKeyPathReference}");
        }
    }
 
    /// <summary>
    /// Indicates whether the key path was actually referenced in the resource configuration.
    /// </summary>
    public bool IsKeyPathReferenced => _trackedKeyPathReference?.WasResolved ?? false;
 
    /// <summary>
    /// Reference expression that will resolve to the path of the server authentication certificate in PFX format.
    /// </summary>
    public required ReferenceExpression PfxPathReference
    {
        get
        {
            return _pfxPathReference!;
        }
        set
        {
            _trackedPfxPathReference = new TrackedReference(value);
            _pfxPathReference = ReferenceExpression.Create($"{_trackedPfxPathReference}");
        }
    }
 
    /// <summary>
    /// Indicates whether the PFX path was actually referenced in the resource configuration.
    /// </summary>
    public bool IsPfxPathReferenced => _trackedPfxPathReference?.WasResolved ?? false;
 
    /// <summary>
    /// The passphrase for the server authentication certificate, if any.
    /// </summary>
    public string? Password { get; init; }
 
    private class TrackedReference : IValueProvider, IManifestExpressionProvider
    {
        private readonly ReferenceExpression _reference;
 
        public TrackedReference(ReferenceExpression reference)
        {
            _reference = reference;
        }
 
        public bool WasResolved { get; internal set; }
 
        public string ValueExpression => _reference.ValueExpression;
 
        public ValueTask<string?> GetValueAsync(CancellationToken cancellationToken = default)
        {
            WasResolved = true;
 
            return _reference.GetValueAsync(cancellationToken);
        }
    }
}
 
/// <summary>
/// Configuration context for server authentication certificate configuration.
/// </summary>
public class HttpsCertificateExecutionConfigurationContext
{
    /// <summary>
    /// Expression that will resolve to the path of the server authentication certificate in PEM format.
    /// For containers this will be a path inside the container.
    /// </summary>
    public required ReferenceExpression CertificatePath { get; init; }
 
    /// <summary>
    /// Expression that will resolve to the path of the server authentication certificate key in PEM format.
    /// For containers this will be a path inside the container.
    /// </summary>
    public required ReferenceExpression KeyPath { get; init; }
 
    /// <summary>
    /// Expression that will resolve to the path of the server authentication certificate in PFX format.
    /// For containers this will be a path inside the container.
    /// </summary>
    public required ReferenceExpression PfxPath { get; init; }
}