@@ -79,3 +79,214 @@ pub fn should_renew_certs_check(config: &crate::config::Config) -> bool {
7979 } ;
8080 expired
8181}
82+
83+ use actix_web:: { App , HttpServer } ;
84+ use instant_acme:: OrderStatus ;
85+ use tracing:: info;
86+
87+ use std:: sync:: mpsc;
88+
89+ /// Starts an HTTP Actix server for HTTPS certificate initialization
90+ pub async fn cert_init_server ( config : & crate :: config:: Config ) -> AtomicServerResult < ( ) > {
91+ let address = format ! ( "{}:{}" , config. opts. ip, config. opts. port) ;
92+ tracing:: warn!( "Server temporarily running in HTTP mode at {}, running Let's Encrypt Certificate initialization..." , address) ;
93+
94+ if config. opts . port != 80 {
95+ tracing:: warn!(
96+ "HTTP port is {}, not 80. Should be 80 in most cases during LetsEncrypt setup." ,
97+ config. opts. port
98+ ) ;
99+ }
100+
101+ let mut well_known_folder = config. static_path . clone ( ) ;
102+ well_known_folder. push ( "well-known" ) ;
103+ fs:: create_dir_all ( & well_known_folder) ?;
104+
105+ let ( tx, rx) = mpsc:: channel ( ) ;
106+
107+ let address_clone = address. clone ( ) ;
108+
109+ std:: thread:: spawn ( move || {
110+ actix_web:: rt:: System :: new ( ) . block_on ( async move {
111+ let init_server = HttpServer :: new ( move || {
112+ App :: new ( ) . service (
113+ actix_files:: Files :: new ( "/.well-known" , well_known_folder. clone ( ) )
114+ . show_files_listing ( ) ,
115+ )
116+ } ) ;
117+
118+ let running_server = init_server. bind ( & address_clone) ?. run ( ) ;
119+
120+ tx. send ( running_server. handle ( ) )
121+ . expect ( "Error sending handle during HTTPS init." ) ;
122+
123+ running_server. await
124+ } )
125+ } ) ;
126+
127+ let handle = rx
128+ . recv ( )
129+ . map_err ( |e| format ! ( "Error receiving handle during HTTPS init. {}" , e) ) ?;
130+
131+ let agent = ureq:: builder ( )
132+ . timeout ( std:: time:: Duration :: from_secs ( 2 ) )
133+ . build ( ) ;
134+
135+ let well_known_url = format ! ( "http://{}/.well-known/" , & config. opts. domain) ;
136+ tracing:: info!( "Testing availability of {}" , & well_known_url) ;
137+ let resp = agent. get ( & well_known_url) . call ( ) . map_err ( |e| {
138+ format ! (
139+ "Unable to send request for Let's Encrypt initialization. {}" ,
140+ e
141+ )
142+ } ) ?;
143+ if resp. status ( ) != 200 {
144+ return Err (
145+ "Server for HTTP initialization not available, returning a non-200 status code" . into ( ) ,
146+ ) ;
147+ } else {
148+ tracing:: info!( "Server for HTTP initialization running correctly" ) ;
149+ }
150+
151+ request_cert ( config)
152+ . await
153+ . map_err ( |e| format ! ( "Certification init failed: {}" , e) ) ?;
154+ tracing:: warn!( "HTTPS TLS Cert init sucesful! Stopping HTTP server, starting HTTPS..." ) ;
155+ handle. stop ( true ) . await ;
156+ Ok ( ( ) )
157+ }
158+
159+ async fn request_cert ( config : & crate :: config:: Config ) -> AtomicServerResult < ( ) > {
160+ // Create a new account. This will generate a fresh ECDSA key for you.
161+ // Alternatively, restore an account from serialized credentials by
162+ // using `Account::from_credentials()`.
163+
164+ let url = if config. opts . development {
165+ instant_acme:: LetsEncrypt :: Staging . url ( )
166+ } else {
167+ instant_acme:: LetsEncrypt :: Production . url ( )
168+ } ;
169+
170+ let email =
171+ config. opts . email . clone ( ) . expect (
172+ "No email set - required for HTTPS certificate initialization with LetsEncrypt" ,
173+ ) ;
174+
175+ info ! ( "Creating LetsEncrypt account with email {}" , email) ;
176+
177+ let account = instant_acme:: Account :: create (
178+ & instant_acme:: NewAccount {
179+ contact : & [ & email] ,
180+ terms_of_service_agreed : true ,
181+ only_return_existing : false ,
182+ } ,
183+ url,
184+ )
185+ . await
186+ . map_err ( |e| format ! ( "Failed to create account: {}" , e) ) ?;
187+
188+ // Create the ACME order based on the given domain names.
189+ // Note that this only needs an `&Account`, so the library will let you
190+ // process multiple orders in parallel for a single account.
191+
192+ let identifier = instant_acme:: Identifier :: Dns ( config. opts . domain . clone ( ) ) ;
193+ let ( mut order, state) = account
194+ . new_order ( & instant_acme:: NewOrder {
195+ identifiers : & [ identifier] ,
196+ } )
197+ . await
198+ . unwrap ( ) ;
199+
200+ tracing:: info!( "order state: {:#?}" , state) ;
201+ assert ! ( matches!( state. status, instant_acme:: OrderStatus :: Pending ) ) ;
202+
203+ // Pick the desired challenge type and prepare the response.
204+
205+ let authorizations = order. authorizations ( & state. authorizations ) . await . unwrap ( ) ;
206+ let mut challenges = Vec :: with_capacity ( authorizations. len ( ) ) ;
207+ for authz in & authorizations {
208+ match authz. status {
209+ instant_acme:: AuthorizationStatus :: Pending => { }
210+ instant_acme:: AuthorizationStatus :: Valid => continue ,
211+ _ => todo ! ( ) ,
212+ }
213+
214+ let challenge = authz
215+ . challenges
216+ . iter ( )
217+ . find ( |c| c. r#type == instant_acme:: ChallengeType :: Http01 )
218+ . ok_or ( "no Http01 challenge found" ) ?;
219+
220+ let instant_acme:: Identifier :: Dns ( identifier) = & authz. identifier ;
221+
222+ println ! ( "Please set the following DNS record then press any key:" ) ;
223+ println ! (
224+ "_acme-challenge.{} IN TXT {}" ,
225+ identifier,
226+ order. key_authorization( challenge) . dns_value( )
227+ ) ;
228+ std:: io:: stdin ( ) . read_line ( & mut String :: new ( ) ) . unwrap ( ) ;
229+
230+ challenges. push ( ( identifier, & challenge. url ) ) ;
231+ }
232+
233+ // Let the server know we're ready to accept the challenges.
234+ for ( _, url) in & challenges {
235+ order. set_challenge_ready ( url) . await . unwrap ( ) ;
236+ }
237+
238+ // Exponentially back off until the order becomes ready or invalid.
239+ let mut tries = 1u8 ;
240+ let mut delay = std:: time:: Duration :: from_millis ( 250 ) ;
241+ let state = loop {
242+ actix:: clock:: sleep ( delay) . await ;
243+ let state = order. state ( ) . await . unwrap ( ) ;
244+ if let instant_acme:: OrderStatus :: Ready | instant_acme:: OrderStatus :: Invalid = state. status
245+ {
246+ tracing:: info!( "order state: {:#?}" , state) ;
247+ break state;
248+ }
249+
250+ delay *= 2 ;
251+ tries += 1 ;
252+ match tries < 5 {
253+ true => info ! ( ?state, tries, "order is not ready, waiting {delay:?}" ) ,
254+ false => {
255+ return Err ( "order is not ready" . into ( ) ) ;
256+ }
257+ }
258+ } ;
259+
260+ if state. status == OrderStatus :: Invalid {
261+ return Err ( "order is invalid" . into ( ) ) ;
262+ }
263+
264+ let mut names = Vec :: with_capacity ( challenges. len ( ) ) ;
265+ for ( identifier, _) in challenges {
266+ names. push ( identifier. to_owned ( ) ) ;
267+ }
268+
269+ // If the order is ready, we can provision the certificate.
270+ // Use the rcgen library to create a Certificate Signing Request.
271+
272+ let mut params = rcgen:: CertificateParams :: new ( names. clone ( ) ) ;
273+ params. distinguished_name = rcgen:: DistinguishedName :: new ( ) ;
274+ let cert = rcgen:: Certificate :: from_params ( params) . unwrap ( ) ;
275+ let csr = cert. serialize_request_der ( ) . map_err ( |e| e. to_string ( ) ) ?;
276+
277+ // Finalize the order and print certificate chain, private key and account credentials.
278+
279+ let cert_chain_pem = order. finalize ( & csr, & state. finalize ) . await . unwrap ( ) ;
280+ info ! ( "certficate chain:\n \n {}" , cert_chain_pem, ) ;
281+ info ! ( "private key:\n \n {}" , cert. serialize_private_key_pem( ) ) ;
282+ info ! (
283+ "account credentials:\n \n {}" ,
284+ serde_json:: to_string_pretty( & account. credentials( ) ) . unwrap( )
285+ ) ;
286+ fs:: write ( & config. cert_path , cert_chain_pem) . expect ( "Unable to write cert file" ) ;
287+ fs:: write ( & config. key_path , cert. serialize_private_key_pem ( ) )
288+ . expect ( "Unable to write key file" ) ;
289+ set_certs_created_at_file ( config) ;
290+
291+ Ok ( ( ) )
292+ }
0 commit comments