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