use std::{future, pin, sync, time}; use crate::domain::{self, acme}; use crate::error; use crate::error::{MapUnexpected, ToUnexpected}; const LETS_ENCRYPT_URL: &'static str = "https://acme-v02.api.letsencrypt.org/directory"; pub type GetHttp01ChallengeKeyError = acme::store::GetHttp01ChallengeKeyError; #[mockall::automock( type SyncDomainFuture=future::Ready>; )] pub trait Manager { type SyncDomainFuture<'mgr>: future::Future> + Send + Unpin + 'mgr where Self: 'mgr; fn sync_domain(&self, domain: domain::Name) -> Self::SyncDomainFuture<'_>; fn get_http01_challenge_key(&self, token: &str) -> Result; } pub trait BoxedManager: Manager + Send + Sync + Clone + 'static {} struct ManagerImpl where Store: acme::store::BoxedStore, { store: Store, account: sync::Arc, } pub async fn new( store: Store, contact_email: &str, ) -> Result where Store: acme::store::BoxedStore, { let dir = acme2::DirectoryBuilder::new(LETS_ENCRYPT_URL.to_string()) .build() .await .map_unexpected()?; let mut builder = acme2::AccountBuilder::new(dir); builder.contact(vec![contact_email.to_string()]); builder.terms_of_service_agreed(true); match store.get_account_key() { Ok(account_key) => { builder.private_key(account_key); } Err(acme::store::GetAccountKeyError::NotFound) => (), Err(acme::store::GetAccountKeyError::Unexpected(err)) => return Err(err.to_unexpected()), } let account = builder.build().await.map_unexpected()?; store .set_account_key(&account.private_key()) .map_unexpected()?; Ok(sync::Arc::new(ManagerImpl { store, account })) } impl BoxedManager for sync::Arc> where Store: acme::store::BoxedStore {} impl Manager for sync::Arc> where Store: acme::store::BoxedStore, { type SyncDomainFuture<'mgr> = pin::Pin> + Send + 'mgr>> where Self: 'mgr; fn sync_domain(&self, domain: domain::Name) -> Self::SyncDomainFuture<'_> { Box::pin(async move { let mut builder = acme2::OrderBuilder::new(self.account.clone()); builder.add_dns_identifier(domain.as_str().to_string()); let order = builder.build().await.map_unexpected()?; let authorizations = order.authorizations().await.map_unexpected()?; for auth in authorizations { let challenge = auth .get_challenge("http-01") .ok_or(error::Unexpected::from("expected http-01 challenge"))?; let challenge_token = challenge .token .as_ref() .ok_or(error::Unexpected::from("expected challenge to have token"))?; let challenge_key = challenge .key_authorization() .map_unexpected()? .ok_or(error::Unexpected::from("expected challenge to have key"))?; self.store .set_http01_challenge_key(challenge_token, &challenge_key) .map_unexpected()?; // At this point the manager is prepared to serve the challenge key via the // `get_http01_challenge_key` method. It is expected that there is some http // server, with this domain pointing at it, which is prepared to serve that // challenge token/key under the `/.well-known/` path. The `validate()` call below // will instigate the acme server to make this check, and block until it succeeds. let challenge = challenge.validate().await.map_unexpected()?; // Poll the challenge every 5 seconds until it is in either the // `valid` or `invalid` state. let challenge = challenge .wait_done(time::Duration::from_secs(5), 3) .await .map_unexpected()?; if challenge.status != acme2::ChallengeStatus::Valid { return Err(error::Unexpected::from( format!( "expected challenge status to be valid, instead it was {:?}", challenge.status ) .as_str(), )); } self.store .del_http01_challenge_key(&challenge_token) .map_unexpected()?; // Poll the authorization every 5 seconds until it is in either the // `valid` or `invalid` state. let authorization = auth .wait_done(time::Duration::from_secs(5), 3) .await .map_unexpected()?; if authorization.status != acme2::AuthorizationStatus::Valid { return Err(error::Unexpected::from( format!( "expected authorization status to be valid, instead it was {:?}", authorization.status, ) .as_str(), )); } } // Poll the order every 5 seconds until it is in either the `ready` or `invalid` state. // Ready means that it is now ready for finalization (certificate creation). let order = order .wait_ready(time::Duration::from_secs(5), 3) .await .map_unexpected()?; if order.status != acme2::OrderStatus::Ready { return Err(error::Unexpected::from( format!( "expected order status to be ready, instead it was {:?}", order.status, ) .as_str(), )); } // Generate an RSA private key for the certificate. let pkey = acme2::gen_rsa_private_key(4096).map_unexpected()?; // Create a certificate signing request for the order, and request // the certificate. let order = order .finalize(acme2::Csr::Automatic(pkey)) .await .map_unexpected()?; // Poll the order every 5 seconds until it is in either the // `valid` or `invalid` state. Valid means that the certificate // has been provisioned, and is now ready for download. let order = order .wait_done(time::Duration::from_secs(5), 3) .await .map_unexpected()?; if order.status != acme2::OrderStatus::Valid { return Err(error::Unexpected::from( format!( "expected order status to be valid, instead it was {:?}", order.status, ) .as_str(), )); } // Download the certificate, and panic if it doesn't exist. let cert = order .certificate() .await .map_unexpected()? .ok_or(error::Unexpected::from( "expected the order to return a certificate", ))?; if cert.len() <= 1 { return Err(error::Unexpected::from( format!( "expected more than one certificate to be returned, instead got {}", cert.len(), ) .as_str(), )); } Ok(()) }) } fn get_http01_challenge_key(&self, token: &str) -> Result { self.store.get_http01_challenge_key(token) } }