From eadb53db0b2bc5c2412d92320c37fbe694cbedde Mon Sep 17 00:00:00 2001 From: Brian Picciano Date: Mon, 21 Aug 2023 19:00:32 +0200 Subject: [PATCH] Implement external domain cert syncing --- README.md | 10 ++ src/domain/acme.rs | 1 + src/domain/acme/account_key_store.rs | 86 +++++++++++ src/domain/acme/manager.rs | 84 ++++++---- src/domain/acme/store.rs | 187 ++--------------------- src/domain/acme/store/direct_fs_store.rs | 70 +++++++++ src/domain/acme/store/json_fs_store.rs | 78 ++++++++++ src/domain/config.rs | 10 ++ src/domain/manager.rs | 51 ++++++- src/domain/tls.rs | 2 +- src/domain/tls/certificate.rs | 44 ++++++ src/main.rs | 35 ++++- src/service/http.rs | 1 + src/util.rs | 21 +++ 14 files changed, 462 insertions(+), 218 deletions(-) create mode 100644 src/domain/acme/account_key_store.rs create mode 100644 src/domain/acme/store/direct_fs_store.rs create mode 100644 src/domain/acme/store/json_fs_store.rs diff --git a/README.md b/README.md index 3a5698d..d6bc438 100644 --- a/README.md +++ b/README.md @@ -118,6 +118,16 @@ domain: # http_url is set. #https_disabled: false + # External domains will have a TLS key/cert generated and signed for them, but + # which will not be served by domani itself. The key/cert files will be placed + # in the configured paths. + # + # HTTPS must be enabled for external_domains to be used. + #external_domains: + #example.com + # tls_key_path: /dir/path/key.pem + # tls_cert_path: /dir/path/cert.pem + service: # Passphrase which must be given by users who are configuring new domains via diff --git a/src/domain/acme.rs b/src/domain/acme.rs index b36ac95..8f1903d 100644 --- a/src/domain/acme.rs +++ b/src/domain/acme.rs @@ -1,2 +1,3 @@ +pub mod account_key_store; pub mod manager; pub mod store; diff --git a/src/domain/acme/account_key_store.rs b/src/domain/acme/account_key_store.rs new file mode 100644 index 0000000..2f108a2 --- /dev/null +++ b/src/domain/acme/account_key_store.rs @@ -0,0 +1,86 @@ +use std::io::{Read, Write}; +use std::{fs, path}; + +use crate::domain::tls::PrivateKey; +use crate::error::unexpected::{self, Mappable}; +use crate::util; + +#[mockall::automock] +pub trait Store { + fn set(&self, k: &PrivateKey) -> Result<(), unexpected::Error>; + fn get(&self) -> unexpected::Result>; +} + +pub struct FSStore { + dir_path: path::PathBuf, +} + +impl FSStore { + pub fn new(dir_path: &path::Path) -> Result { + fs::create_dir_all(dir_path).or_unexpected()?; + Ok(Self { + dir_path: dir_path.into(), + }) + } + + fn account_key_path(&self) -> path::PathBuf { + self.dir_path.join("account.key") + } +} + +impl Store for FSStore { + fn set(&self, k: &PrivateKey) -> Result<(), unexpected::Error> { + let path = self.account_key_path(); + { + let mut file = fs::File::create(&path).or_unexpected_while("creating file")?; + file.write_all(k.to_string().as_bytes()) + .or_unexpected_while("writing file") + } + .map_unexpected_while(|| format!("path is {}", path.display()))?; + Ok(()) + } + + fn get(&self) -> unexpected::Result> { + let path = self.account_key_path(); + { + let mut file = + match util::open_file(path.as_path()).or_unexpected_while("opening_file")? { + Some(file) => file, + None => return Ok(None), + }; + + let mut key = String::new(); + file.read_to_string(&mut key) + .or_unexpected_while("reading file")?; + + let key: PrivateKey = key.parse().or_unexpected_while("parsing private key")?; + + unexpected::Result::>::Ok(Some(key)) + } + .map_unexpected_while(|| format!("path is {}", path.display())) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::domain::tls; + use tempdir::TempDir; + + #[test] + fn account_key() { + let tmp_dir = TempDir::new("domain_acme_account_key_store").unwrap(); + let store = FSStore::new(tmp_dir.path()).expect("store created"); + + assert!(matches!( + store.get(), + unexpected::Result::>::Ok(None) + )); + + let k = tls::PrivateKey::new(); + + store.set(&k).expect("account private key set"); + + assert_eq!(Some(k), store.get().expect("account private key retrieved")); + } +} diff --git a/src/domain/acme/manager.rs b/src/domain/acme/manager.rs index 7eca505..2fcf576 100644 --- a/src/domain/acme/manager.rs +++ b/src/domain/acme/manager.rs @@ -1,8 +1,8 @@ use std::{sync, time}; use crate::domain::acme; -use crate::domain::tls::{Certificate, PrivateKey}; -use crate::error::unexpected::{self, Intoable, Mappable}; +use crate::domain::tls::{Certificate, CertificateChain, PrivateKey}; +use crate::error::unexpected::{self, Mappable}; use crate::{domain, token, util}; const LETS_ENCRYPT_URL: &str = "https://acme-v02.api.letsencrypt.org/directory"; @@ -16,39 +16,53 @@ pub enum GetHttp01ChallengeKeyError { Unexpected(#[from] unexpected::Error), } -pub type GetCertificateError = acme::store::GetCertificateError; - #[mockall::automock] pub trait Manager { fn sync_domain<'mgr>( &'mgr self, domain: domain::Name, - ) -> util::BoxFuture<'mgr, Result<(), unexpected::Error>>; + external_config: Option, + ) -> util::BoxFuture<'mgr, unexpected::Result<()>>; fn get_http01_challenge_key(&self, token: &str) -> Result; - /// Returned vec is guaranteed to have len > 0 fn get_certificate( &self, domain: &str, - ) -> Result<(PrivateKey, Vec), GetCertificateError>; + ) -> unexpected::Result>; } pub struct ManagerImpl { store: Box, token_store: Box, account: sync::Arc, + external_store_fn: Box< + dyn Fn( + &domain::ConfigExternalDomain, + ) -> unexpected::Result> + + Send + + Sync, + >, } impl ManagerImpl { - pub async fn new( + pub async fn new( store: AcmeStore, token_store: TokenStore, + account_key_store: AccountKeyStore, + external_store_fn: ExternalAcmeStoreFn, config: &domain::ConfigACME, - ) -> Result + ) -> unexpected::Result where + ExternalAcmeStoreFn: Fn( + &domain::ConfigExternalDomain, + ) -> unexpected::Result> + + Send + + Sync + + 'static, AcmeStore: acme::store::Store + Send + Sync + 'static, TokenStore: token::Store + Send + Sync + 'static, + AccountKeyStore: acme::account_key_store::Store + Send + Sync + 'static, { let dir = acme2::DirectoryBuilder::new(LETS_ENCRYPT_URL.to_string()) .build() @@ -62,18 +76,15 @@ impl ManagerImpl { builder.contact(vec![contact]); builder.terms_of_service_agreed(true); - match store.get_account_key() { - Ok(account_key) => { - builder.private_key( - (&account_key) - .try_into() - .or_unexpected_while("parsing private key")?, - ); - } - Err(acme::store::GetAccountKeyError::NotFound) => (), - Err(acme::store::GetAccountKeyError::Unexpected(err)) => { - return Err(err.into_unexpected()) - } + if let Some(account_key) = account_key_store + .get() + .or_unexpected_while("fetching account key")? + { + builder.private_key( + (&account_key) + .try_into() + .or_unexpected_while("parsing private key")?, + ); } let account = builder @@ -87,14 +98,15 @@ impl ManagerImpl { .try_into() .or_unexpected_while("parsing private key back out")?; - store - .set_account_key(&account_key) + account_key_store + .set(&account_key) .or_unexpected_while("storing account key")?; Ok(Self { store: Box::from(store), token_store: Box::from(token_store), account, + external_store_fn: Box::from(external_store_fn), }) } } @@ -103,16 +115,26 @@ impl Manager for ManagerImpl { fn sync_domain<'mgr>( &'mgr self, domain: domain::Name, + external_config: Option, ) -> util::BoxFuture<'mgr, Result<(), unexpected::Error>> { Box::pin(async move { + let external_store; + let store = match external_config { + None => &self.store, + Some(config) => { + external_store = (self.external_store_fn)(&config).or_unexpected()?; + &external_store + } + }; + // 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()) { + if let Ok(Some((_, certs))) = 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 cert_with_soonest_not_after = cert + let cert_with_soonest_not_after = certs .into_iter() .map(|cert| openssl::x509::X509::try_from(&cert)) .try_collect::>() @@ -280,7 +302,7 @@ impl Manager for ManagerImpl { // Download the certificate, and panic if it doesn't exist. log::info!("Fetching certificate for domain {}", domain.as_str()); - let cert = order + let certs = order .certificate() .await .or_unexpected_while("fetching certificate")? @@ -292,19 +314,19 @@ impl Manager for ManagerImpl { .try_collect::>() .or_unexpected_while("parsing certificate")?; - if cert.len() <= 1 { + if certs.len() <= 1 { return Err(unexpected::Error::from( format!( "expected more than one certificate to be returned, instead got {}", - cert.len(), + certs.len(), ) .as_str(), )); } log::info!("Certificate for {} successfully retrieved", domain.as_str()); - self.store - .set_certificate(domain.as_str(), pkey, cert) + store + .set_certificate(domain.as_str(), pkey, certs.into()) .or_unexpected_while("storing new cert")?; Ok(()) @@ -323,7 +345,7 @@ impl Manager for ManagerImpl { fn get_certificate( &self, domain: &str, - ) -> Result<(PrivateKey, Vec), GetCertificateError> { + ) -> unexpected::Result> { self.store.get_certificate(domain) } } diff --git a/src/domain/acme/store.rs b/src/domain/acme/store.rs index 5f1ad27..dc93e04 100644 --- a/src/domain/acme/store.rs +++ b/src/domain/acme/store.rs @@ -1,193 +1,24 @@ -use std::io::{Read, Write}; -use std::str::FromStr; -use std::{fs, path}; +mod json_fs_store; +pub use json_fs_store::*; -use crate::domain::tls::{Certificate, PrivateKey}; -use crate::error::unexpected::{self, Mappable}; -use crate::util; +mod direct_fs_store; +pub use direct_fs_store::*; -use serde::{Deserialize, Serialize}; - -#[derive(thiserror::Error, Debug)] -pub enum GetAccountKeyError { - #[error("not found")] - NotFound, - - #[error(transparent)] - Unexpected(#[from] unexpected::Error), -} - -#[derive(thiserror::Error, Debug)] -pub enum GetCertificateError { - #[error("not found")] - NotFound, - - #[error(transparent)] - Unexpected(#[from] unexpected::Error), -} +use crate::domain::tls::{CertificateChain, PrivateKey}; +use crate::error::unexpected; #[mockall::automock] pub trait Store { - fn set_account_key(&self, k: &PrivateKey) -> Result<(), unexpected::Error>; - fn get_account_key(&self) -> Result; - fn set_certificate( &self, domain: &str, key: PrivateKey, - cert: Vec, + cert: CertificateChain, ) -> Result<(), unexpected::Error>; - /// Returned vec is guaranteed to have len > 0 + /// Returned chain is guaranteed to have len > 0 fn get_certificate( &self, domain: &str, - ) -> Result<(PrivateKey, Vec), GetCertificateError>; -} - -#[derive(Debug, Serialize, Deserialize)] -struct StoredPKeyCert { - private_key: PrivateKey, - cert: Vec, -} - -pub struct FSStore { - dir_path: path::PathBuf, -} - -impl FSStore { - pub fn new(dir_path: &path::Path) -> Result { - vec![ - dir_path, - dir_path.join("http01_challenge_keys").as_ref(), - dir_path.join("certificates").as_ref(), - ] - .iter() - .map(|dir| { - fs::create_dir_all(dir) - .map_unexpected_while(|| format!("creating dir {}", dir.display())) - }) - .try_collect()?; - - Ok(Self { - dir_path: dir_path.into(), - }) - } - - fn account_key_path(&self) -> path::PathBuf { - self.dir_path.join("account.key") - } - - fn certificate_path(&self, domain: &str) -> path::PathBuf { - let mut domain = domain.to_string(); - domain.push_str(".json"); - - self.dir_path.join("certificates").join(domain) - } -} - -impl Store for FSStore { - fn set_account_key(&self, k: &PrivateKey) -> Result<(), unexpected::Error> { - let path = self.account_key_path(); - { - let mut file = fs::File::create(&path).or_unexpected_while("creating file")?; - file.write_all(k.to_string().as_bytes()) - .or_unexpected_while("writing file") - } - .map_unexpected_while(|| format!("path is {}", path.display()))?; - Ok(()) - } - - fn get_account_key(&self) -> Result { - let path = self.account_key_path(); - { - let mut file = - match util::open_file(path.as_path()).or_unexpected_while("opening_file")? { - Some(file) => file, - None => return Err(GetAccountKeyError::NotFound), - }; - - let mut key = String::new(); - file.read_to_string(&mut key) - .or_unexpected_while("reading file")?; - - let key = PrivateKey::from_str(&key).or_unexpected_while("parsing private key")?; - - Ok::(key) - } - .map_unexpected_while(|| format!("path is {}", path.display())) - .map_err(|err| err.into()) - } - - fn set_certificate( - &self, - domain: &str, - key: PrivateKey, - cert: Vec, - ) -> Result<(), unexpected::Error> { - let to_store = StoredPKeyCert { - private_key: key, - cert, - }; - - let path = self.certificate_path(domain); - { - let cert_file = - fs::File::create(path.as_path()).or_unexpected_while("creating file")?; - serde_json::to_writer(cert_file, &to_store).or_unexpected_while("writing cert to file") - } - .map_unexpected_while(|| format!("path is {}", path.display())) - } - - fn get_certificate( - &self, - domain: &str, - ) -> Result<(PrivateKey, Vec), GetCertificateError> { - let path = self.certificate_path(domain); - { - let file = match util::open_file(path.as_path()).or_unexpected_while("opening_file")? { - Some(file) => file, - None => return Err(GetCertificateError::NotFound), - }; - - let stored: StoredPKeyCert = - serde_json::from_reader(file).or_unexpected_while("parsing json")?; - - Ok::<(PrivateKey, Vec), unexpected::Error>(( - stored.private_key, - stored.cert, - )) - } - .map_unexpected_while(|| format!("path is {}", path.display())) - .map_err(|err| err.into()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::domain::tls; - use tempdir::TempDir; - - #[test] - fn account_key() { - let tmp_dir = TempDir::new("domain_acme_store_account_key").unwrap(); - let store = FSStore::new(tmp_dir.path()).expect("store created"); - - assert!(matches!( - store.get_account_key(), - Err::(GetAccountKeyError::NotFound) - )); - - let k = tls::PrivateKey::new(); - - store.set_account_key(&k).expect("account private key set"); - - assert_eq!( - k, - store - .get_account_key() - .expect("account private key retrieved") - ); - } + ) -> unexpected::Result>; } diff --git a/src/domain/acme/store/direct_fs_store.rs b/src/domain/acme/store/direct_fs_store.rs new file mode 100644 index 0000000..c5be0c9 --- /dev/null +++ b/src/domain/acme/store/direct_fs_store.rs @@ -0,0 +1,70 @@ +use std::{fs, path}; + +use crate::domain::tls::{CertificateChain, PrivateKey}; +use crate::error::unexpected::{self, Mappable}; +use crate::util; + +pub struct DirectFSStore { + key_file_path: path::PathBuf, + cert_file_path: path::PathBuf, +} + +impl DirectFSStore { + pub fn new( key_file_path: &path::Path, cert_file_path: &path::Path,) -> Self { + Self { + key_file_path: key_file_path.into(), + cert_file_path: cert_file_path.into(), + } + } +} + +impl super::Store for DirectFSStore { + fn set_certificate( + &self, + _domain: &str, + key: PrivateKey, + cert: CertificateChain, + ) -> unexpected::Result<()> { + fs::write(&self.key_file_path, key.to_string()).map_unexpected_while(|| { + format!("writing private key to {}", &self.key_file_path.display()) + })?; + + fs::write(&self.cert_file_path, cert.to_string()).map_unexpected_while(|| { + format!("writing certificate to {}", &self.cert_file_path.display()) + })?; + + Ok(()) + } + + fn get_certificate( + &self, + _domain: &str, + ) -> unexpected::Result> { + let key: Option = + util::parse_file(&self.key_file_path).map_unexpected_while(|| { + format!("reading private key from {}", &self.key_file_path.display()) + })?; + + let certs: Option = util::parse_file(&self.cert_file_path) + .map_unexpected_while(|| { + format!( + "reading certificate from {}", + &self.cert_file_path.display() + ) + })?; + + if key.is_none() != certs.is_none() { + } + + match (key, certs) { + (None, None) => Ok(None), + (Some(key), Some(certs)) => Ok(Some((key, certs))), + _ => + Err(unexpected::Error::from(format!( + "private key file {} and cert file {} are in inconsistent state, one exists but the other doesn't", + &self.key_file_path.display(), + &self.cert_file_path.display(), + ).as_str())) + } + } +} diff --git a/src/domain/acme/store/json_fs_store.rs b/src/domain/acme/store/json_fs_store.rs new file mode 100644 index 0000000..34dd4f8 --- /dev/null +++ b/src/domain/acme/store/json_fs_store.rs @@ -0,0 +1,78 @@ +use std::{fs, path}; + +use crate::domain::tls::{Certificate, CertificateChain, PrivateKey}; +use crate::error::unexpected::{self, Mappable}; +use crate::util; + +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize)] +struct StoredPKeyCert { + private_key: PrivateKey, + cert: Vec, +} + +pub struct JSONFSStore { + dir_path: path::PathBuf, +} + +impl JSONFSStore { + pub fn new(dir_path: &path::Path) -> unexpected::Result { + fs::create_dir_all(dir_path).or_unexpected()?; + Ok(Self { + dir_path: dir_path.into(), + }) + } + + fn certificate_path(&self, domain: &str) -> path::PathBuf { + let mut domain = domain.to_string(); + domain.push_str(".json"); + + self.dir_path.join(domain) + } +} + +impl super::Store for JSONFSStore { + fn set_certificate( + &self, + domain: &str, + key: PrivateKey, + certs: CertificateChain, + ) -> Result<(), unexpected::Error> { + let to_store = StoredPKeyCert { + private_key: key, + cert: certs.into(), + }; + + let path = self.certificate_path(domain); + { + let cert_file = + fs::File::create(path.as_path()).or_unexpected_while("creating file")?; + serde_json::to_writer(cert_file, &to_store).or_unexpected_while("writing cert to file") + } + .map_unexpected_while(|| format!("path is {}", path.display())) + } + + fn get_certificate( + &self, + domain: &str, + ) -> unexpected::Result> { + let path = self.certificate_path(domain); + { + let file = match util::open_file(path.as_path()).or_unexpected_while("opening_file")? { + Some(file) => file, + None => return Ok(None), + }; + + let stored: StoredPKeyCert = + serde_json::from_reader(file).or_unexpected_while("parsing json")?; + + unexpected::Result::>::Ok(Some(( + stored.private_key, + stored.cert.into(), + ))) + } + .map_unexpected_while(|| format!("path is {}", path.display())) + .map_err(|err| err.into()) + } +} diff --git a/src/domain/config.rs b/src/domain/config.rs index 0f11fca..e56e59f 100644 --- a/src/domain/config.rs +++ b/src/domain/config.rs @@ -58,6 +58,13 @@ pub struct ConfigProxiedDomain { pub https_disabled: bool, } +#[serde_as] +#[derive(Clone, Deserialize, Serialize)] +pub struct ConfigExternalDomain { + pub tls_key_path: path::PathBuf, + pub tls_cert_path: path::PathBuf, +} + #[derive(Clone, Deserialize, Serialize)] pub struct Config { pub store_dir_path: path::PathBuf, @@ -73,4 +80,7 @@ pub struct Config { #[serde(default = "default_interface_domain")] pub interface_domain: Option, + + #[serde(default)] + pub external_domains: collections::HashMap, } diff --git a/src/domain/manager.rs b/src/domain/manager.rs index ddbd9e7..31465f1 100644 --- a/src/domain/manager.rs +++ b/src/domain/manager.rs @@ -10,6 +10,7 @@ pub enum GetSettingsResult { Builtin(domain::config::ConfigBuiltinDomain), Proxied(domain::config::ConfigProxiedDomain), Interface, + External(domain::config::ConfigExternalDomain), } #[derive(thiserror::Error, Debug)] @@ -222,7 +223,22 @@ impl ManagerImpl { async fn sync_domain_https_cert(&self, domain: &domain::Name) -> unexpected::Result<()> { if let Some(ref acme_manager) = self.acme_manager { log::info!("Syncing HTTPS certificate for domain {domain}"); - acme_manager.sync_domain(domain.clone()).await?; + acme_manager.sync_domain(domain.clone(), None).await?; + } + + Ok(()) + } + + async fn sync_external_domain( + &self, + domain: &domain::Name, + config: &domain::ConfigExternalDomain, + ) -> unexpected::Result<()> { + if let Some(ref acme_manager) = self.acme_manager { + log::info!("Syncing HTTPS certificate for external domain {domain}"); + acme_manager + .sync_domain(domain.clone(), Some(config.clone())) + .await?; } Ok(()) @@ -247,6 +263,14 @@ impl ManagerImpl { GetSettingsResult::Proxied(config) => (None, !config.https_disabled, false), GetSettingsResult::Interface => (None, true, false), + + // External domains do their own thing, separate from the rest of this flow. + GetSettingsResult::External(config) => { + self.sync_external_domain(&domain, &config) + .await + .map_unexpected_while(|| format!("syncing external domain {domain}"))?; + continue; + } }; if let Some(settings) = settings { @@ -301,6 +325,10 @@ impl Manager for ManagerImpl { return Ok(GetSettingsResult::Proxied(config.clone())); } + if let Some(config) = self.config.external_domains.get(domain) { + return Ok(GetSettingsResult::External(config.clone())); + } + Ok(GetSettingsResult::Stored(self.domain_store.get(domain)?)) } @@ -322,6 +350,9 @@ impl Manager for ManagerImpl { unexpected::Error::from("can't call get_file on interface domain").into(), ); } + GetSettingsResult::External(_) => { + return Err(GetFileError::DomainNotFound); + } }; let path = settings.process_path(path); @@ -342,8 +373,9 @@ impl Manager for ManagerImpl { let is_interface = Some(&domain) == self.config.interface_domain.as_ref(); let is_builtin = self.config.builtin_domains.contains_key(&domain); let is_proxied = self.config.proxied_domains.contains_key(&domain); + let is_external = self.config.external_domains.contains_key(&domain); - if is_interface || is_builtin || is_proxied { + if is_interface || is_builtin || is_proxied || is_external { return Err(SyncWithSettingsError::NotModifiable); } @@ -422,6 +454,15 @@ impl Manager for ManagerImpl { }) } + self.config + .external_domains + .iter() + .map(|(domain, _)| ManagedDomain { + domain: domain.clone(), + public: false, + }) + .collect_into(&mut res); + Ok(res) } } @@ -442,12 +483,12 @@ impl rustls::server::ResolvesServerCert for HttpsCertResolver { let domain = client_hello.server_name()?; match (self.0).acme_manager.as_ref()?.get_certificate(domain) { - Err(acme::manager::GetCertificateError::NotFound) => { + Err(err) => Err(err), + Ok(None) => { log::warn!("No cert found for domain {domain}"); Ok(None) } - Err(acme::manager::GetCertificateError::Unexpected(err)) => Err(err), - Ok((key, cert)) => { + Ok(Some((key, cert))) => { match rustls::sign::any_supported_type(&key.into()).or_unexpected() { Err(err) => Err(err), Ok(key) => Ok(Some(sync::Arc::new(rustls::sign::CertifiedKey { diff --git a/src/domain/tls.rs b/src/domain/tls.rs index 181a805..2b8d630 100644 --- a/src/domain/tls.rs +++ b/src/domain/tls.rs @@ -2,4 +2,4 @@ mod private_key; pub use self::private_key::PrivateKey; mod certificate; -pub use self::certificate::Certificate; +pub use self::certificate::{Certificate, CertificateChain}; diff --git a/src/domain/tls/certificate.rs b/src/domain/tls/certificate.rs index 0e8ee9a..9a78cf4 100644 --- a/src/domain/tls/certificate.rs +++ b/src/domain/tls/certificate.rs @@ -113,3 +113,47 @@ impl From for rustls::Certificate { rustls::Certificate(c.0) } } + +pub struct CertificateChain(Vec); + +impl FromStr for CertificateChain { + type Err = pem::PemError; + + fn from_str(s: &str) -> Result { + Ok(CertificateChain( + pem::parse_many(s)? + .into_iter() + .map(|s| Certificate(s.into_contents())) + .collect(), + )) + } +} + +impl fmt::Display for CertificateChain { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for cert in &self.0 { + pem::Pem::new("CERTIFICATE", cert.0.clone()).fmt(f)?; + } + Ok(()) + } +} + +impl From for Vec { + fn from(c: CertificateChain) -> Self { + c.0 + } +} + +impl From> for CertificateChain { + fn from(v: Vec) -> Self { + Self(v) + } +} + +impl std::iter::IntoIterator for CertificateChain { + type Item = Certificate; + type IntoIter = std::vec::IntoIter; + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} diff --git a/src/main.rs b/src/main.rs index 02f9b92..457206f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -77,6 +77,10 @@ async fn main() { } } + if config.domain.external_domains.len() > 0 && config.service.http.https_addr.is_none() { + panic!("https must be enabled to use external_domains") + } + config }; @@ -109,14 +113,39 @@ async fn main() { .clone() .expect("acme configuration must be set if https is enabled"); - let domain_acme_store = - domani::domain::acme::store::FSStore::new(&config.domain.store_dir_path.join("acme")) - .expect("domain acme store initialization failed"); + let acme_dir_path = config.domain.store_dir_path.join("acme"); + + let domain_acme_store = { + let dir_path = acme_dir_path.join("certificates"); + domani::domain::acme::store::JSONFSStore::new(&dir_path).unwrap_or_else(|e| { + panic!( + "failed to initialize acme cert store at {}: {e}", + dir_path.display() + ) + }) + }; + + let domain_acme_account_key_store = domani::domain::acme::account_key_store::FSStore::new( + &acme_dir_path, + ) + .unwrap_or_else(|e| { + panic!( + "failed to initialize account key store at {}: {e}", + acme_dir_path.display() + ) + }); Some( domani::domain::acme::manager::ManagerImpl::new( domain_acme_store, domani::token::MemStore::new(), + domain_acme_account_key_store, + |config| { + Ok(Box::from(domani::domain::acme::store::DirectFSStore::new( + &config.tls_key_path, + &config.tls_cert_path, + ))) + }, &acme_config, ) .await diff --git a/src/service/http.rs b/src/service/http.rs index 931d388..607f8b2 100644 --- a/src/service/http.rs +++ b/src/service/http.rs @@ -238,6 +238,7 @@ impl Service { } Ok(domain::manager::GetSettingsResult::Proxied(_)) => None, Ok(domain::manager::GetSettingsResult::Interface) => None, + Ok(domain::manager::GetSettingsResult::External(_)) => None, Err(domain::manager::GetSettingsError::NotFound) => None, Err(domain::manager::GetSettingsError::Unexpected(e)) => { return self.internal_error( diff --git a/src/util.rs b/src/util.rs index e8674ac..e354dbf 100644 --- a/src/util.rs +++ b/src/util.rs @@ -10,6 +10,27 @@ pub fn open_file(path: &path::Path) -> io::Result> { } } +#[derive(thiserror::Error, Debug)] +pub enum ParseFileError { + #[error("io error: {0}")] + IO(io::Error), + + #[error("parse error")] + FromStr(ParseError), +} + +pub fn parse_file( + path: &path::Path, +) -> Result, ParseFileError> { + match fs::read_to_string(path) { + Ok(s) => Ok(Some(s.parse().map_err(|e| ParseFileError::FromStr(e))?)), + Err(err) => match err.kind() { + io::ErrorKind::NotFound => Ok(None), + _ => Err(ParseFileError::IO(err)), + }, + } +} + pub type BoxByteStream = futures::stream::BoxStream<'static, io::Result>; pub type BoxFuture<'a, O> = pin::Pin + Send + 'a>>;