1+ using System . Diagnostics ;
2+ using Aspire . Hosting . ApplicationModel ;
3+ using Microsoft . Extensions . DependencyInjection ;
4+ using Microsoft . Extensions . Hosting ;
5+ using Microsoft . Extensions . Logging ;
6+
7+ namespace Aspire . Hosting ;
8+
9+ internal static class DevCertHostingExtensions
10+ {
11+ /// <summary>
12+ /// Injects the ASP.NET Core HTTPS developer certificate into the resource via the specified environment variables when
13+ /// <paramref name="builder"/>.<see cref="IResourceBuilder{T}.ApplicationBuilder">ApplicationBuilder</see>.<see cref="IDistributedApplicationBuilder.ExecutionContext">ExecutionContext</see>.<see cref="DistributedApplicationExecutionContext.IsRunMode">IsRunMode</see><c> == true</c>.<br/>
14+ /// If the resource is a <see cref="ContainerResource"/>, the certificate files will be bind mounted into the container.
15+ /// </summary>
16+ /// <remarks>
17+ /// This method <strong>does not</strong> configure an HTTPS endpoint on the resource.
18+ /// Use <see cref="ResourceBuilderExtensions.WithHttpsEndpoint{TResource}"/> to configure an HTTPS endpoint.
19+ /// </remarks>
20+ public static IResourceBuilder < TResource > RunWithHttpsDevCertificate < TResource > (
21+ this IResourceBuilder < TResource > builder , string certFileEnv , string certKeyFileEnv , Action < string , string > ? onSuccessfulExport = null )
22+ where TResource : IResourceWithEnvironment
23+ {
24+ if ( builder . ApplicationBuilder . ExecutionContext . IsRunMode && builder . ApplicationBuilder . Environment . IsDevelopment ( ) )
25+ {
26+ builder . ApplicationBuilder . Eventing . Subscribe < BeforeStartEvent > ( async ( e , ct ) =>
27+ {
28+ var logger = e . Services . GetRequiredService < ResourceLoggerService > ( ) . GetLogger ( builder . Resource ) ;
29+
30+ // Export the ASP.NET Core HTTPS development certificate & private key to files and configure the resource to use them via
31+ // the specified environment variables.
32+ var ( exported , certPath , certKeyPath ) = await TryExportDevCertificateAsync ( builder . ApplicationBuilder , logger ) ;
33+
34+ if ( ! exported )
35+ {
36+ // The export failed for some reason, don't configure the resource to use the certificate.
37+ return ;
38+ }
39+
40+ if ( builder . Resource is ContainerResource containerResource )
41+ {
42+ // Bind-mount the certificate files into the container.
43+ const string DEV_CERT_BIND_MOUNT_DEST_DIR = "/dev-certs" ;
44+
45+ var certFileName = Path . GetFileName ( certPath ) ;
46+ var certKeyFileName = Path . GetFileName ( certKeyPath ) ;
47+
48+ var bindSource = Path . GetDirectoryName ( certPath ) ?? throw new UnreachableException ( ) ;
49+
50+ var certFileDest = $ "{ DEV_CERT_BIND_MOUNT_DEST_DIR } /{ certFileName } ";
51+ var certKeyFileDest = $ "{ DEV_CERT_BIND_MOUNT_DEST_DIR } /{ certKeyFileName } ";
52+
53+ builder . ApplicationBuilder . CreateResourceBuilder ( containerResource )
54+ . WithBindMount ( bindSource , DEV_CERT_BIND_MOUNT_DEST_DIR , isReadOnly : true )
55+ . WithEnvironment ( certFileEnv , certFileDest )
56+ . WithEnvironment ( certKeyFileEnv , certKeyFileDest ) ;
57+ }
58+ else
59+ {
60+ builder
61+ . WithEnvironment ( certFileEnv , certPath )
62+ . WithEnvironment ( certKeyFileEnv , certKeyPath ) ;
63+ }
64+
65+ if ( onSuccessfulExport is not null )
66+ {
67+ onSuccessfulExport ( certPath , certKeyPath ) ;
68+ }
69+ } ) ;
70+ }
71+
72+ return builder ;
73+ }
74+
75+ private static async Task < ( bool , string CertFilePath , string CertKeyFilPath ) > TryExportDevCertificateAsync ( IDistributedApplicationBuilder builder , ILogger logger )
76+ {
77+ // Exports the ASP.NET Core HTTPS development certificate & private key to PEM files using 'dotnet dev-certs https' to a temporary
78+ // directory and returns the path.
79+ // TODO: Check if we're running on a platform that already has the cert and key exported to a file (e.g. macOS) and just use those instead.
80+ var appNameHash = builder . Configuration [ "AppHost:Sha256" ] ! [ ..10 ] ;
81+ var tempDir = Path . Combine ( Path . GetTempPath ( ) , $ "aspire.{ appNameHash } ") ;
82+ var certExportPath = Path . Combine ( tempDir , "dev-cert.pem" ) ;
83+ var certKeyExportPath = Path . Combine ( tempDir , "dev-cert.key" ) ;
84+
85+ if ( File . Exists ( certExportPath ) && File . Exists ( certKeyExportPath ) )
86+ {
87+ // Certificate already exported, return the path.
88+ logger . LogDebug ( "Using previously exported dev cert files '{CertPath}' and '{CertKeyPath}'" , certExportPath , certKeyExportPath ) ;
89+ return ( true , certExportPath , certKeyExportPath ) ;
90+ }
91+
92+ if ( File . Exists ( certExportPath ) )
93+ {
94+ logger . LogTrace ( "Deleting previously exported dev cert file '{CertPath}'" , certExportPath ) ;
95+ File . Delete ( certExportPath ) ;
96+ }
97+
98+ if ( File . Exists ( certKeyExportPath ) )
99+ {
100+ logger . LogTrace ( "Deleting previously exported dev cert key file '{CertKeyPath}'" , certKeyExportPath ) ;
101+ File . Delete ( certKeyExportPath ) ;
102+ }
103+
104+ if ( ! Directory . Exists ( tempDir ) )
105+ {
106+ logger . LogTrace ( "Creating directory to export dev cert to '{ExportDir}'" , tempDir ) ;
107+ Directory . CreateDirectory ( tempDir ) ;
108+ }
109+
110+ string [ ] args = [ "dev-certs" , "https" , "--export-path" , $ "\" { certExportPath } \" ", "--format" , "Pem" , "--no-password" ] ;
111+ var argsString = string . Join ( ' ' , args ) ;
112+
113+ logger . LogTrace ( "Running command to export dev cert: {ExportCmd}" , $ "dotnet { argsString } ") ;
114+ var exportStartInfo = new ProcessStartInfo
115+ {
116+ FileName = "dotnet" ,
117+ Arguments = argsString ,
118+ RedirectStandardOutput = true ,
119+ RedirectStandardError = true ,
120+ UseShellExecute = false ,
121+ CreateNoWindow = true ,
122+ WindowStyle = ProcessWindowStyle . Hidden ,
123+ } ;
124+
125+ var exportProcess = new Process { StartInfo = exportStartInfo } ;
126+
127+ Task ? stdOutTask = null ;
128+ Task ? stdErrTask = null ;
129+
130+ try
131+ {
132+ try
133+ {
134+ if ( exportProcess . Start ( ) )
135+ {
136+ stdOutTask = ConsumeOutput ( exportProcess . StandardOutput , msg => logger . LogInformation ( "> {StandardOutput}" , msg ) ) ;
137+ stdErrTask = ConsumeOutput ( exportProcess . StandardError , msg => logger . LogError ( "! {ErrorOutput}" , msg ) ) ;
138+ }
139+ }
140+ catch ( Exception ex )
141+ {
142+ logger . LogError ( ex , "Failed to start HTTPS dev certificate export process" ) ;
143+ return default ;
144+ }
145+
146+ var timeout = TimeSpan . FromSeconds ( 5 ) ;
147+ var exited = exportProcess . WaitForExit ( timeout ) ;
148+
149+ if ( exited && File . Exists ( certExportPath ) && File . Exists ( certKeyExportPath ) )
150+ {
151+ logger . LogDebug ( "Dev cert exported to '{CertPath}' and '{CertKeyPath}'" , certExportPath , certKeyExportPath ) ;
152+ return ( true , certExportPath , certKeyExportPath ) ;
153+ }
154+
155+ if ( exportProcess . HasExited && exportProcess . ExitCode != 0 )
156+ {
157+ logger . LogError ( "HTTPS dev certificate export failed with exit code {ExitCode}" , exportProcess . ExitCode ) ;
158+ }
159+ else if ( ! exportProcess . HasExited )
160+ {
161+ exportProcess . Kill ( true ) ;
162+ logger . LogError ( "HTTPS dev certificate export timed out after {TimeoutSeconds} seconds" , timeout . TotalSeconds ) ;
163+ }
164+ else
165+ {
166+ logger . LogError ( "HTTPS dev certificate export failed for an unknown reason" ) ;
167+ }
168+ return default ;
169+ }
170+ finally
171+ {
172+ await Task . WhenAll ( stdOutTask ?? Task . CompletedTask , stdErrTask ?? Task . CompletedTask ) ;
173+ }
174+
175+ static async Task ConsumeOutput ( TextReader reader , Action < string > callback )
176+ {
177+ char [ ] buffer = new char [ 256 ] ;
178+ int charsRead ;
179+
180+ while ( ( charsRead = await reader . ReadAsync ( buffer , 0 , buffer . Length ) ) > 0 )
181+ {
182+ callback ( new string ( buffer , 0 , charsRead ) ) ;
183+ }
184+ }
185+ }
186+ }
0 commit comments