File: src\Shared\DcpConnectionMaterial.cs
Web Access
Project: src\src\Aspire.Hosting.Native.Server\Aspire.Hosting.Native.Server.csproj (Aspire.Hosting.Native.Server)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Diagnostics;
using System.Net;
using System.Net.Http.Headers;
using System.Net.Security;
using System.Net.Sockets;
using System.Security.Authentication;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
 
namespace Aspire.Shared;
 
/// <summary>Owns fully parsed and authenticated connection material from a local DCP kubeconfig.</summary>
internal sealed class DcpConnectionMaterial : IDisposable
{
    internal const string ActivitySourceName = "Aspire.Dcp.Connection";
    private static readonly ActivitySource s_activities = new(ActivitySourceName);
    private readonly X509Certificate2 _authority;
    private readonly X509Certificate2? _certificate;
    public HttpClient Client { get; }
 
    private DcpConnectionMaterial(DcpKubeconfigData configuration, TimeSpan requestTimeout)
    {
        if (!Uri.TryCreate(configuration.Server, UriKind.Absolute, out var server) ||
            server.Scheme != Uri.UriSchemeHttps || !server.IsLoopback)
        {
            throw new InvalidDataException("DCP must advertise a loopback HTTPS endpoint.");
        }
        _authority = X509CertificateLoader.LoadCertificate(Convert.FromBase64String(configuration.CertificateAuthorityData!));
        HttpClientHandler? handler = null;
        try
        {
            handler = new HttpClientHandler
            {
                ServerCertificateCustomValidationCallback = (_, certificate, chain, errors) =>
                {
                    if (certificate is null || chain is null || (errors & SslPolicyErrors.RemoteCertificateNameMismatch) != 0)
                    {
                        return false;
                    }
                    chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust;
                    chain.ChainPolicy.CustomTrustStore.Add(_authority);
                    chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
 
                    return chain.Build(certificate);
                }
            };
            if (configuration.ClientCertificateData is not null && configuration.ClientKeyData is not null)
            {
                _certificate = X509Certificate2.CreateFromPem(
                    Encoding.UTF8.GetString(Convert.FromBase64String(configuration.ClientCertificateData)),
                    Encoding.UTF8.GetString(Convert.FromBase64String(configuration.ClientKeyData)));
                handler.ClientCertificates.Add(_certificate);
            }
            Client = new HttpClient(handler) { BaseAddress = server, Timeout = requestTimeout };
            if (configuration.Token is not null)
            {
                Client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", configuration.Token);
            }
        }
        catch
        {
            handler?.Dispose();
            _certificate?.Dispose();
            _authority.Dispose();
            throw;
        }
    }
 
    public static Task<DcpConnectionMaterial> WaitAsync(string kubeconfig, Func<bool> hasExited,
        TimeSpan requestTimeout, TimeSpan retryInterval, Action<string> diagnostic, CancellationToken cancellationToken)
        => WaitCoreAsync(kubeconfig, hasExited, requestTimeout, retryInterval, diagnostic,
            TimeProvider.System, requestTimeout, cancellationToken);
 
    public static Task<DcpConnectionMaterial> WaitAsync(string kubeconfig, Func<bool> hasExited,
        TimeSpan requestTimeout, TimeSpan retryInterval, Action<string> diagnostic, TimeProvider timeProvider,
        CancellationToken cancellationToken)
        => WaitCoreAsync(kubeconfig, hasExited, requestTimeout, retryInterval, diagnostic,
            timeProvider, Timeout.InfiniteTimeSpan, cancellationToken);
 
    private static async Task<DcpConnectionMaterial> WaitCoreAsync(string kubeconfig, Func<bool> hasExited,
        TimeSpan requestTimeout, TimeSpan retryInterval, Action<string> diagnostic, TimeProvider timeProvider,
        TimeSpan clientTimeout, CancellationToken cancellationToken)
    {
        // Only classified readiness/authentication metadata is traced. Never
        // attach kubeconfig paths, credentials, certificate bytes or HTTP bodies.
        using var activity = s_activities.StartActivity("dcp.connection.ready");
        var attempts = 0;
        try
        {
            while (true)
            {
                attempts++;
                cancellationToken.ThrowIfCancellationRequested();
                if (hasExited())
                {
                    throw new InvalidOperationException("DCP exited before publishing authenticated connection material.");
                }
                DcpConnectionMaterial? material = null;
                try
                {
                    // DCP writes scalar kubeconfig values such as:
                    //   certificate-authority-data: LS0t...
                    // A nonempty value can still be a partial base64/PEM write. Parse
                    // certificates and authenticate /readyz inside the retry boundary.
                    if (File.Exists(kubeconfig))
                    {
                        var configuration = DcpKubeconfigData.Parse(
                            await File.ReadAllTextAsync(kubeconfig, cancellationToken).ConfigureAwait(false));
                        if (configuration.Server is not null && configuration.CertificateAuthorityData is not null &&
                            (configuration.Token is not null ||
                             configuration.ClientCertificateData is not null && configuration.ClientKeyData is not null))
                        {
                            // HttpClient.Timeout cannot change after the first request.
                            // Legacy consumers retain their configured client budget;
                            // provider-aware consumers own each request deadline.
                            material = new DcpConnectionMaterial(configuration, clientTimeout);
                            using var timeout = new CancellationTokenSource(requestTimeout, timeProvider);
                            using var request = CancellationTokenSource.CreateLinkedTokenSource(timeout.Token, cancellationToken);
                            using var response = await material.Client.GetAsync("/readyz", request.Token).ConfigureAwait(false);
                            if (response.IsSuccessStatusCode)
                            {
                                activity?.SetTag("dcp.authentication", configuration.Token is null ? "client_certificate" : "bearer");
                                activity?.SetStatus(ActivityStatusCode.Ok);
                                return material;
                            }
                            if (response.StatusCode is not (HttpStatusCode.ServiceUnavailable or HttpStatusCode.Unauthorized or HttpStatusCode.Forbidden))
                            {
                                throw new InvalidOperationException($"DCP readiness failed ({(int)response.StatusCode}).");
                            }
                        }
                    }
                }
                catch (Exception exception) when (!hasExited() &&
                    exception is FormatException or CryptographicException or AuthenticationException or IOException ||
                    !hasExited() && exception is HttpRequestException
                    {
                        InnerException: SocketException { SocketErrorCode: SocketError.ConnectionRefused } or AuthenticationException
                    })
                {
                    diagnostic($"Waiting for complete DCP connection material: {exception.GetType().Name}.");
                }
                catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested && !hasExited())
                {
                    diagnostic("Waiting for the DCP readiness request to complete.");
                }
                catch
                {
                    material?.Dispose();
                    throw;
                }
                material?.Dispose();
                diagnostic("Waiting for authenticated DCP readiness.");
                await Task.Delay(retryInterval, timeProvider, cancellationToken).ConfigureAwait(false);
            }
        }
        catch (Exception exception)
        {
            activity?.SetStatus(ActivityStatusCode.Error);
            activity?.SetTag("error.type", exception.GetType().FullName);
            throw;
        }
        finally
        {
            activity?.SetTag("dcp.readiness.attempts", attempts);
        }
    }
 
    public void Dispose()
    {
        Client.Dispose();
        _certificate?.Dispose();
        _authority.Dispose();
    }
}