diff --git a/.env.dev b/.env.dev index 8de4c39..59bb4b3 100644 --- a/.env.dev +++ b/.env.dev @@ -4,4 +4,3 @@ export DOMIPLY_ORIGIN_STORE_GIT_DIR_PATH=/tmp/domiply_dev_env/origin/git export DOMIPLY_DOMAIN_CHECKER_TARGET_A=127.0.0.1 export DOMIPLY_DOMAIN_CONFIG_STORE_DIR_PATH=/tmp/domiply_dev_env/domain/config export DOMIPLY_DOMAIN_ACME_STORE_DIR_PATH=/tmp/domiply_dev_env/domain/acme -export DOMIPLY_DOMAIN_ACME_CONTACT_EMAIL=domiply@example.com diff --git a/src/domain/acme/manager.rs b/src/domain/acme/manager.rs index b1f4ddb..17f5800 100644 --- a/src/domain/acme/manager.rs +++ b/src/domain/acme/manager.rs @@ -46,8 +46,11 @@ where .await .map_unexpected()?; + let mut contact = String::from("mailto:"); + contact.push_str(contact_email); + let mut builder = acme2::AccountBuilder::new(dir); - builder.contact(vec![contact_email.to_string()]); + builder.contact(vec![contact]); builder.terms_of_service_agreed(true); match store.get_account_key() { @@ -77,6 +80,25 @@ where where Self: 'mgr; fn sync_domain(&self, domain: domain::Name) -> Self::SyncDomainFuture<'_> { + // if there's an existing cert, and its expiry (determined by the soonest value of + // not_after amongst its parts) is later than 30 days from now, then we consider it to be + // synced. + if let Ok(cert) = self.store.get_certificate(domain.as_str()) { + let thirty_days = openssl::asn1::Asn1Time::days_from_now(30) + .expect("parsed thirty days from now as Asn1Time"); + + let soonest_not_after = cert[1..] + .into_iter() + .map(|cert_part| cert_part.not_after()) + .fold(cert[0].not_after(), |a, b| if a < b { a } else { b }); + + if thirty_days < soonest_not_after { + return Box::pin(future::ready(Ok(()))); + } + } + + println!("fetching a new certificate for domain {}", domain.as_str()); + Box::pin(async move { let mut builder = acme2::OrderBuilder::new(self.account.clone()); builder.add_dns_identifier(domain.as_str().to_string()); @@ -106,18 +128,27 @@ where // 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. + // challenge token/key under the `/.well-known/acme-challenge` path. The + // `validate()` call below will instigate the acme server to make this check, and + // block until it succeeds. + println!( + "waiting for ACME challenge to be validated for domain {}", + domain.as_str(), + ); 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 + let challenge_res = challenge.wait_done(time::Duration::from_secs(5), 3).await; + + // no matter what the result is, clean up the challenge key + self.store + .del_http01_challenge_key(&challenge_token) .map_unexpected()?; + let challenge = challenge_res.map_unexpected()?; + if challenge.status != acme2::ChallengeStatus::Valid { return Err(error::Unexpected::from( format!( @@ -128,12 +159,13 @@ where )); } - 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. + println!( + "waiting for ACME authorization to be validated for domain {}", + domain.as_str(), + ); + let authorization = auth .wait_done(time::Duration::from_secs(5), 3) .await @@ -152,6 +184,11 @@ where // 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). + println!( + "waiting for ACME order to be made ready for domain {}", + domain.as_str(), + ); + let order = order .wait_ready(time::Duration::from_secs(5), 3) .await @@ -180,6 +217,10 @@ where // 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. + println!( + "waiting for ACME order to be validated for domain {}", + domain.as_str(), + ); let order = order .wait_done(time::Duration::from_secs(5), 3) .await @@ -196,6 +237,7 @@ where } // Download the certificate, and panic if it doesn't exist. + println!("fetching certificate for domain {}", domain.as_str()); let cert = order .certificate() @@ -215,6 +257,11 @@ where )); } + println!("certificate for {} successfully retrieved", domain.as_str()); + self.store + .set_certificate(domain.as_str(), cert) + .map_unexpected()?; + Ok(()) }) } diff --git a/src/main.rs b/src/main.rs index 2c2ce52..24aff4a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -28,7 +28,8 @@ struct Cli { #[arg( long, help = "E.g. '[::]:443', if given then SSL certs will automatically be retrieved for all domains using LetsEncrypt", - env = "DOMIPLY_HTTPS_LISTEN_ADDR" + env = "DOMIPLY_HTTPS_LISTEN_ADDR", + requires = "domain_acme_contact_email" )] https_listen_addr: Option, @@ -50,8 +51,8 @@ struct Cli { #[arg(long, required = true, env = "DOMIPLY_DOMAIN_ACME_STORE_DIR_PATH")] domain_acme_store_dir_path: path::PathBuf, - #[arg(long, required = true, env = "DOMIPLY_DOMAIN_ACME_CONTACT_EMAIL")] - domain_acme_contact_email: String, + #[arg(long, env = "DOMIPLY_DOMAIN_ACME_CONTACT_EMAIL")] + domain_acme_contact_email: Option, } fn main() { @@ -102,13 +103,14 @@ fn main() { domiply::domain::acme::store::new(&config.domain_acme_store_dir_path) .expect("domain acme store initialized"); + // if https_listen_addr is set then domain_acme_contact_email is required, see the Cli/clap + // settings. + let domain_acme_contact_email = config.domain_acme_contact_email.unwrap(); + let domain_acme_manager = tokio_runtime.block_on(async { - domiply::domain::acme::manager::new( - domain_acme_store, - &config.domain_acme_contact_email, - ) - .await - .expect("domain acme manager initialized") + domiply::domain::acme::manager::new(domain_acme_store, &domain_acme_contact_email) + .await + .expect("domain acme manager initialized") }); Some(domain_acme_manager) diff --git a/src/service.rs b/src/service.rs index 5827279..a70430a 100644 --- a/src/service.rs +++ b/src/service.rs @@ -341,8 +341,8 @@ where return svc.render(200, path, ()); } - if method == Method::GET && path.starts_with("/.well-known/") { - let token = path.trim_start_matches("/.well-known/"); + if method == Method::GET && path.starts_with("/.well-known/acme-challenge/") { + let token = path.trim_start_matches("/.well-known/acme-challenge/"); if let Ok(key) = svc.domain_manager.get_acme_http01_challenge_key(token) { let body: hyper::Body = key.into();