File: Certificates\SdkCertificateToolRunner.cs
Web Access
Project: src\src\Aspire.Cli\Aspire.Cli.csproj (aspire)
// 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.Text;
using System.Text.Json;
using Aspire.Cli.DotNet;
using Microsoft.Extensions.Logging;
 
namespace Aspire.Cli.Certificates;
 
/// <summary>
/// Certificate tool runner that uses the global dotnet SDK's dev-certs command.
/// </summary>
internal sealed class SdkCertificateToolRunner(ILogger<SdkCertificateToolRunner> logger) : ICertificateToolRunner
{
    public async Task<(int ExitCode, CertificateTrustResult? Result)> CheckHttpCertificateMachineReadableAsync(
        DotNetCliRunnerInvocationOptions options,
        CancellationToken cancellationToken)
    {
        var outputBuilder = new StringBuilder();
 
        var startInfo = new ProcessStartInfo("dotnet")
        {
            WorkingDirectory = Environment.CurrentDirectory,
            UseShellExecute = false,
            CreateNoWindow = true,
            RedirectStandardOutput = true,
            RedirectStandardError = true
        };
 
        startInfo.ArgumentList.Add("dev-certs");
        startInfo.ArgumentList.Add("https");
        startInfo.ArgumentList.Add("--check-trust-machine-readable");
 
        using var process = new Process { StartInfo = startInfo };
 
        process.OutputDataReceived += (sender, e) =>
        {
            if (e.Data is not null)
            {
                outputBuilder.AppendLine(e.Data);
                options.StandardOutputCallback?.Invoke(e.Data);
            }
        };
 
        process.ErrorDataReceived += (sender, e) =>
        {
            if (e.Data is not null)
            {
                options.StandardErrorCallback?.Invoke(e.Data);
            }
        };
 
        process.Start();
        process.BeginOutputReadLine();
        process.BeginErrorReadLine();
 
        await process.WaitForExitAsync(cancellationToken);
 
        var exitCode = process.ExitCode;
 
        // Parse the JSON output
        try
        {
            var jsonOutput = outputBuilder.ToString().Trim();
            if (string.IsNullOrEmpty(jsonOutput))
            {
                return (exitCode, new CertificateTrustResult
                {
                    HasCertificates = false,
                    TrustLevel = null,
                    Certificates = []
                });
            }
 
            var certificates = JsonSerializer.Deserialize(jsonOutput, Aspire.Cli.JsonSourceGenerationContext.Default.ListDevCertInfo);
            if (certificates is null || certificates.Count == 0)
            {
                return (exitCode, new CertificateTrustResult
                {
                    HasCertificates = false,
                    TrustLevel = null,
                    Certificates = []
                });
            }
 
            // Find the highest versioned valid certificate
            var now = DateTimeOffset.Now;
            var validCertificates = certificates
                .Where(c => c.IsHttpsDevelopmentCertificate && c.ValidityNotBefore <= now && now <= c.ValidityNotAfter)
                .OrderByDescending(c => c.Version)
                .ToList();
 
            var highestVersionedCert = validCertificates.FirstOrDefault();
            var trustLevel = highestVersionedCert?.TrustLevel;
 
            return (exitCode, new CertificateTrustResult
            {
                HasCertificates = validCertificates.Count > 0,
                TrustLevel = trustLevel,
                Certificates = certificates
            });
        }
        catch (JsonException ex)
        {
            logger.LogDebug(ex, "Failed to parse dev-certs machine-readable output");
            return (exitCode, null);
        }
    }
 
    public async Task<int> TrustHttpCertificateAsync(
        DotNetCliRunnerInvocationOptions options,
        CancellationToken cancellationToken)
    {
        var startInfo = new ProcessStartInfo("dotnet")
        {
            WorkingDirectory = Environment.CurrentDirectory,
            UseShellExecute = false,
            CreateNoWindow = true,
            RedirectStandardOutput = true,
            RedirectStandardError = true
        };
 
        startInfo.ArgumentList.Add("dev-certs");
        startInfo.ArgumentList.Add("https");
        startInfo.ArgumentList.Add("--trust");
 
        using var process = new Process { StartInfo = startInfo };
 
        process.OutputDataReceived += (sender, e) =>
        {
            if (e.Data is not null)
            {
                options.StandardOutputCallback?.Invoke(e.Data);
            }
        };
 
        process.ErrorDataReceived += (sender, e) =>
        {
            if (e.Data is not null)
            {
                options.StandardErrorCallback?.Invoke(e.Data);
            }
        };
 
        process.Start();
        process.BeginOutputReadLine();
        process.BeginErrorReadLine();
 
        await process.WaitForExitAsync(cancellationToken);
 
        return process.ExitCode;
    }
}