Implement external domain cert syncing

This commit is contained in:
Brian Picciano 2023-08-21 19:00:32 +02:00
parent 49ac208286
commit eadb53db0b
14 changed files with 462 additions and 218 deletions

View File

@ -118,6 +118,16 @@ domain:
# http_url is set. # http_url is set.
#https_disabled: false #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: service:
# Passphrase which must be given by users who are configuring new domains via # Passphrase which must be given by users who are configuring new domains via

View File

@ -1,2 +1,3 @@
pub mod account_key_store;
pub mod manager; pub mod manager;
pub mod store; pub mod store;

View File

@ -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<Option<PrivateKey>>;
}
pub struct FSStore {
dir_path: path::PathBuf,
}
impl FSStore {
pub fn new(dir_path: &path::Path) -> Result<Self, unexpected::Error> {
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<Option<PrivateKey>> {
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::<Option<PrivateKey>>::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::<Option<PrivateKey>>::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"));
}
}

View File

@ -1,8 +1,8 @@
use std::{sync, time}; use std::{sync, time};
use crate::domain::acme; use crate::domain::acme;
use crate::domain::tls::{Certificate, PrivateKey}; use crate::domain::tls::{Certificate, CertificateChain, PrivateKey};
use crate::error::unexpected::{self, Intoable, Mappable}; use crate::error::unexpected::{self, Mappable};
use crate::{domain, token, util}; use crate::{domain, token, util};
const LETS_ENCRYPT_URL: &str = "https://acme-v02.api.letsencrypt.org/directory"; const LETS_ENCRYPT_URL: &str = "https://acme-v02.api.letsencrypt.org/directory";
@ -16,39 +16,53 @@ pub enum GetHttp01ChallengeKeyError {
Unexpected(#[from] unexpected::Error), Unexpected(#[from] unexpected::Error),
} }
pub type GetCertificateError = acme::store::GetCertificateError;
#[mockall::automock] #[mockall::automock]
pub trait Manager { pub trait Manager {
fn sync_domain<'mgr>( fn sync_domain<'mgr>(
&'mgr self, &'mgr self,
domain: domain::Name, domain: domain::Name,
) -> util::BoxFuture<'mgr, Result<(), unexpected::Error>>; external_config: Option<domain::ConfigExternalDomain>,
) -> util::BoxFuture<'mgr, unexpected::Result<()>>;
fn get_http01_challenge_key(&self, token: &str) -> Result<String, GetHttp01ChallengeKeyError>; fn get_http01_challenge_key(&self, token: &str) -> Result<String, GetHttp01ChallengeKeyError>;
/// Returned vec is guaranteed to have len > 0
fn get_certificate( fn get_certificate(
&self, &self,
domain: &str, domain: &str,
) -> Result<(PrivateKey, Vec<Certificate>), GetCertificateError>; ) -> unexpected::Result<Option<(PrivateKey, CertificateChain)>>;
} }
pub struct ManagerImpl { pub struct ManagerImpl {
store: Box<dyn acme::store::Store + Send + Sync>, store: Box<dyn acme::store::Store + Send + Sync>,
token_store: Box<dyn token::Store + Send + Sync>, token_store: Box<dyn token::Store + Send + Sync>,
account: sync::Arc<acme2::Account>, account: sync::Arc<acme2::Account>,
external_store_fn: Box<
dyn Fn(
&domain::ConfigExternalDomain,
) -> unexpected::Result<Box<dyn acme::store::Store + Send + Sync>>
+ Send
+ Sync,
>,
} }
impl ManagerImpl { impl ManagerImpl {
pub async fn new<AcmeStore, TokenStore>( pub async fn new<ExternalAcmeStoreFn, AcmeStore, TokenStore, AccountKeyStore>(
store: AcmeStore, store: AcmeStore,
token_store: TokenStore, token_store: TokenStore,
account_key_store: AccountKeyStore,
external_store_fn: ExternalAcmeStoreFn,
config: &domain::ConfigACME, config: &domain::ConfigACME,
) -> Result<Self, unexpected::Error> ) -> unexpected::Result<Self>
where where
ExternalAcmeStoreFn: Fn(
&domain::ConfigExternalDomain,
) -> unexpected::Result<Box<dyn acme::store::Store + Send + Sync>>
+ Send
+ Sync
+ 'static,
AcmeStore: acme::store::Store + Send + Sync + 'static, AcmeStore: acme::store::Store + Send + Sync + 'static,
TokenStore: token::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()) let dir = acme2::DirectoryBuilder::new(LETS_ENCRYPT_URL.to_string())
.build() .build()
@ -62,19 +76,16 @@ impl ManagerImpl {
builder.contact(vec![contact]); builder.contact(vec![contact]);
builder.terms_of_service_agreed(true); builder.terms_of_service_agreed(true);
match store.get_account_key() { if let Some(account_key) = account_key_store
Ok(account_key) => { .get()
.or_unexpected_while("fetching account key")?
{
builder.private_key( builder.private_key(
(&account_key) (&account_key)
.try_into() .try_into()
.or_unexpected_while("parsing private key")?, .or_unexpected_while("parsing private key")?,
); );
} }
Err(acme::store::GetAccountKeyError::NotFound) => (),
Err(acme::store::GetAccountKeyError::Unexpected(err)) => {
return Err(err.into_unexpected())
}
}
let account = builder let account = builder
.build() .build()
@ -87,14 +98,15 @@ impl ManagerImpl {
.try_into() .try_into()
.or_unexpected_while("parsing private key back out")?; .or_unexpected_while("parsing private key back out")?;
store account_key_store
.set_account_key(&account_key) .set(&account_key)
.or_unexpected_while("storing account key")?; .or_unexpected_while("storing account key")?;
Ok(Self { Ok(Self {
store: Box::from(store), store: Box::from(store),
token_store: Box::from(token_store), token_store: Box::from(token_store),
account, account,
external_store_fn: Box::from(external_store_fn),
}) })
} }
} }
@ -103,16 +115,26 @@ impl Manager for ManagerImpl {
fn sync_domain<'mgr>( fn sync_domain<'mgr>(
&'mgr self, &'mgr self,
domain: domain::Name, domain: domain::Name,
external_config: Option<domain::ConfigExternalDomain>,
) -> util::BoxFuture<'mgr, Result<(), unexpected::Error>> { ) -> util::BoxFuture<'mgr, Result<(), unexpected::Error>> {
Box::pin(async move { 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 // 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 // not_after amongst its parts) is later than 30 days from now, then we consider it to
// be synced. // 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) let thirty_days = openssl::asn1::Asn1Time::days_from_now(30)
.expect("parsed thirty days from now as Asn1Time"); .expect("parsed thirty days from now as Asn1Time");
let cert_with_soonest_not_after = cert let cert_with_soonest_not_after = certs
.into_iter() .into_iter()
.map(|cert| openssl::x509::X509::try_from(&cert)) .map(|cert| openssl::x509::X509::try_from(&cert))
.try_collect::<Vec<openssl::x509::X509>>() .try_collect::<Vec<openssl::x509::X509>>()
@ -280,7 +302,7 @@ impl Manager for ManagerImpl {
// Download the certificate, and panic if it doesn't exist. // Download the certificate, and panic if it doesn't exist.
log::info!("Fetching certificate for domain {}", domain.as_str()); log::info!("Fetching certificate for domain {}", domain.as_str());
let cert = order let certs = order
.certificate() .certificate()
.await .await
.or_unexpected_while("fetching certificate")? .or_unexpected_while("fetching certificate")?
@ -292,19 +314,19 @@ impl Manager for ManagerImpl {
.try_collect::<Vec<Certificate>>() .try_collect::<Vec<Certificate>>()
.or_unexpected_while("parsing certificate")?; .or_unexpected_while("parsing certificate")?;
if cert.len() <= 1 { if certs.len() <= 1 {
return Err(unexpected::Error::from( return Err(unexpected::Error::from(
format!( format!(
"expected more than one certificate to be returned, instead got {}", "expected more than one certificate to be returned, instead got {}",
cert.len(), certs.len(),
) )
.as_str(), .as_str(),
)); ));
} }
log::info!("Certificate for {} successfully retrieved", domain.as_str()); log::info!("Certificate for {} successfully retrieved", domain.as_str());
self.store store
.set_certificate(domain.as_str(), pkey, cert) .set_certificate(domain.as_str(), pkey, certs.into())
.or_unexpected_while("storing new cert")?; .or_unexpected_while("storing new cert")?;
Ok(()) Ok(())
@ -323,7 +345,7 @@ impl Manager for ManagerImpl {
fn get_certificate( fn get_certificate(
&self, &self,
domain: &str, domain: &str,
) -> Result<(PrivateKey, Vec<Certificate>), GetCertificateError> { ) -> unexpected::Result<Option<(PrivateKey, CertificateChain)>> {
self.store.get_certificate(domain) self.store.get_certificate(domain)
} }
} }

View File

@ -1,193 +1,24 @@
use std::io::{Read, Write}; mod json_fs_store;
use std::str::FromStr; pub use json_fs_store::*;
use std::{fs, path};
use crate::domain::tls::{Certificate, PrivateKey}; mod direct_fs_store;
use crate::error::unexpected::{self, Mappable}; pub use direct_fs_store::*;
use crate::util;
use serde::{Deserialize, Serialize}; use crate::domain::tls::{CertificateChain, PrivateKey};
use crate::error::unexpected;
#[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),
}
#[mockall::automock] #[mockall::automock]
pub trait Store { pub trait Store {
fn set_account_key(&self, k: &PrivateKey) -> Result<(), unexpected::Error>;
fn get_account_key(&self) -> Result<PrivateKey, GetAccountKeyError>;
fn set_certificate( fn set_certificate(
&self, &self,
domain: &str, domain: &str,
key: PrivateKey, key: PrivateKey,
cert: Vec<Certificate>, cert: CertificateChain,
) -> Result<(), unexpected::Error>; ) -> Result<(), unexpected::Error>;
/// Returned vec is guaranteed to have len > 0 /// Returned chain is guaranteed to have len > 0
fn get_certificate( fn get_certificate(
&self, &self,
domain: &str, domain: &str,
) -> Result<(PrivateKey, Vec<Certificate>), GetCertificateError>; ) -> unexpected::Result<Option<(PrivateKey, CertificateChain)>>;
}
#[derive(Debug, Serialize, Deserialize)]
struct StoredPKeyCert {
private_key: PrivateKey,
cert: Vec<Certificate>,
}
pub struct FSStore {
dir_path: path::PathBuf,
}
impl FSStore {
pub fn new(dir_path: &path::Path) -> Result<Self, unexpected::Error> {
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<PrivateKey, GetAccountKeyError> {
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::<PrivateKey, unexpected::Error>(key)
}
.map_unexpected_while(|| format!("path is {}", path.display()))
.map_err(|err| err.into())
}
fn set_certificate(
&self,
domain: &str,
key: PrivateKey,
cert: Vec<Certificate>,
) -> 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<Certificate>), 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<Certificate>), 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::<PrivateKey, GetAccountKeyError>(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")
);
}
} }

View File

@ -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<Option<(PrivateKey, CertificateChain)>> {
let key: Option<PrivateKey> =
util::parse_file(&self.key_file_path).map_unexpected_while(|| {
format!("reading private key from {}", &self.key_file_path.display())
})?;
let certs: Option<CertificateChain> = 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()))
}
}
}

View File

@ -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<Certificate>,
}
pub struct JSONFSStore {
dir_path: path::PathBuf,
}
impl JSONFSStore {
pub fn new(dir_path: &path::Path) -> unexpected::Result<Self> {
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<Option<(PrivateKey, CertificateChain)>> {
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::<Option<(PrivateKey, CertificateChain)>>::Ok(Some((
stored.private_key,
stored.cert.into(),
)))
}
.map_unexpected_while(|| format!("path is {}", path.display()))
.map_err(|err| err.into())
}
}

View File

@ -58,6 +58,13 @@ pub struct ConfigProxiedDomain {
pub https_disabled: bool, 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)] #[derive(Clone, Deserialize, Serialize)]
pub struct Config { pub struct Config {
pub store_dir_path: path::PathBuf, pub store_dir_path: path::PathBuf,
@ -73,4 +80,7 @@ pub struct Config {
#[serde(default = "default_interface_domain")] #[serde(default = "default_interface_domain")]
pub interface_domain: Option<domain::Name>, pub interface_domain: Option<domain::Name>,
#[serde(default)]
pub external_domains: collections::HashMap<domain::Name, ConfigExternalDomain>,
} }

View File

@ -10,6 +10,7 @@ pub enum GetSettingsResult {
Builtin(domain::config::ConfigBuiltinDomain), Builtin(domain::config::ConfigBuiltinDomain),
Proxied(domain::config::ConfigProxiedDomain), Proxied(domain::config::ConfigProxiedDomain),
Interface, Interface,
External(domain::config::ConfigExternalDomain),
} }
#[derive(thiserror::Error, Debug)] #[derive(thiserror::Error, Debug)]
@ -222,7 +223,22 @@ impl ManagerImpl {
async fn sync_domain_https_cert(&self, domain: &domain::Name) -> unexpected::Result<()> { async fn sync_domain_https_cert(&self, domain: &domain::Name) -> unexpected::Result<()> {
if let Some(ref acme_manager) = self.acme_manager { if let Some(ref acme_manager) = self.acme_manager {
log::info!("Syncing HTTPS certificate for domain {domain}"); 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(()) Ok(())
@ -247,6 +263,14 @@ impl ManagerImpl {
GetSettingsResult::Proxied(config) => (None, !config.https_disabled, false), GetSettingsResult::Proxied(config) => (None, !config.https_disabled, false),
GetSettingsResult::Interface => (None, true, 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 { if let Some(settings) = settings {
@ -301,6 +325,10 @@ impl Manager for ManagerImpl {
return Ok(GetSettingsResult::Proxied(config.clone())); 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)?)) 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(), unexpected::Error::from("can't call get_file on interface domain").into(),
); );
} }
GetSettingsResult::External(_) => {
return Err(GetFileError::DomainNotFound);
}
}; };
let path = settings.process_path(path); 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_interface = Some(&domain) == self.config.interface_domain.as_ref();
let is_builtin = self.config.builtin_domains.contains_key(&domain); let is_builtin = self.config.builtin_domains.contains_key(&domain);
let is_proxied = self.config.proxied_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); 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) Ok(res)
} }
} }
@ -442,12 +483,12 @@ impl rustls::server::ResolvesServerCert for HttpsCertResolver {
let domain = client_hello.server_name()?; let domain = client_hello.server_name()?;
match (self.0).acme_manager.as_ref()?.get_certificate(domain) { 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}"); log::warn!("No cert found for domain {domain}");
Ok(None) Ok(None)
} }
Err(acme::manager::GetCertificateError::Unexpected(err)) => Err(err), Ok(Some((key, cert))) => {
Ok((key, cert)) => {
match rustls::sign::any_supported_type(&key.into()).or_unexpected() { match rustls::sign::any_supported_type(&key.into()).or_unexpected() {
Err(err) => Err(err), Err(err) => Err(err),
Ok(key) => Ok(Some(sync::Arc::new(rustls::sign::CertifiedKey { Ok(key) => Ok(Some(sync::Arc::new(rustls::sign::CertifiedKey {

View File

@ -2,4 +2,4 @@ mod private_key;
pub use self::private_key::PrivateKey; pub use self::private_key::PrivateKey;
mod certificate; mod certificate;
pub use self::certificate::Certificate; pub use self::certificate::{Certificate, CertificateChain};

View File

@ -113,3 +113,47 @@ impl From<Certificate> for rustls::Certificate {
rustls::Certificate(c.0) rustls::Certificate(c.0)
} }
} }
pub struct CertificateChain(Vec<Certificate>);
impl FromStr for CertificateChain {
type Err = pem::PemError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
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<CertificateChain> for Vec<Certificate> {
fn from(c: CertificateChain) -> Self {
c.0
}
}
impl From<Vec<Certificate>> for CertificateChain {
fn from(v: Vec<Certificate>) -> Self {
Self(v)
}
}
impl std::iter::IntoIterator for CertificateChain {
type Item = Certificate;
type IntoIter = std::vec::IntoIter<Self::Item>;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}

View File

@ -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 config
}; };
@ -109,14 +113,39 @@ async fn main() {
.clone() .clone()
.expect("acme configuration must be set if https is enabled"); .expect("acme configuration must be set if https is enabled");
let domain_acme_store = let acme_dir_path = config.domain.store_dir_path.join("acme");
domani::domain::acme::store::FSStore::new(&config.domain.store_dir_path.join("acme"))
.expect("domain acme store initialization failed"); 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( Some(
domani::domain::acme::manager::ManagerImpl::new( domani::domain::acme::manager::ManagerImpl::new(
domain_acme_store, domain_acme_store,
domani::token::MemStore::new(), 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, &acme_config,
) )
.await .await

View File

@ -238,6 +238,7 @@ impl Service {
} }
Ok(domain::manager::GetSettingsResult::Proxied(_)) => None, Ok(domain::manager::GetSettingsResult::Proxied(_)) => None,
Ok(domain::manager::GetSettingsResult::Interface) => None, Ok(domain::manager::GetSettingsResult::Interface) => None,
Ok(domain::manager::GetSettingsResult::External(_)) => None,
Err(domain::manager::GetSettingsError::NotFound) => None, Err(domain::manager::GetSettingsError::NotFound) => None,
Err(domain::manager::GetSettingsError::Unexpected(e)) => { Err(domain::manager::GetSettingsError::Unexpected(e)) => {
return self.internal_error( return self.internal_error(

View File

@ -10,6 +10,27 @@ pub fn open_file(path: &path::Path) -> io::Result<Option<fs::File>> {
} }
} }
#[derive(thiserror::Error, Debug)]
pub enum ParseFileError<ParseError> {
#[error("io error: {0}")]
IO(io::Error),
#[error("parse error")]
FromStr(ParseError),
}
pub fn parse_file<T: std::str::FromStr>(
path: &path::Path,
) -> Result<Option<T>, ParseFileError<T::Err>> {
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<bytes::Bytes>>; pub type BoxByteStream = futures::stream::BoxStream<'static, io::Result<bytes::Bytes>>;
pub type BoxFuture<'a, O> = pin::Pin<Box<dyn futures::Future<Output = O> + Send + 'a>>; pub type BoxFuture<'a, O> = pin::Pin<Box<dyn futures::Future<Output = O> + Send + 'a>>;