Compare commits
No commits in common. "209daacf1be4502f65d0b76ca9540ead5f290175" and "6d8799ce8c7eff26cc98e518d8931f3197f22e74" have entirely different histories.
209daacf1b
...
6d8799ce8c
1
.env.dev
1
.env.dev
@ -4,3 +4,4 @@ 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_CHECKER_TARGET_A=127.0.0.1
|
||||||
export DOMIPLY_DOMAIN_CONFIG_STORE_DIR_PATH=/tmp/domiply_dev_env/domain/config
|
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_STORE_DIR_PATH=/tmp/domiply_dev_env/domain/acme
|
||||||
|
export DOMIPLY_DOMAIN_ACME_CONTACT_EMAIL=domiply@example.com
|
||||||
|
11
TODO
11
TODO
@ -1,11 +0,0 @@
|
|||||||
- make acme store implement https://docs.rs/rustls/latest/rustls/server/trait.ResolvesServerCert.html
|
|
||||||
|
|
||||||
- pass that into https://docs.rs/rustls/latest/rustls/struct.ConfigBuilder.html#
|
|
||||||
|
|
||||||
- turn that into a TlsAcceptor (From is implemented here:
|
|
||||||
https://docs.rs/tokio-rustls/latest/tokio_rustls/struct.TlsAcceptor.html#impl-From%3CArc%3CServerConfig%3E%3E-for-TlsAcceptor)
|
|
||||||
|
|
||||||
- use tls-listener crate to wrap hyper accepter: https://github.com/tmccombs/tls-listener/blob/main/examples/http.rs#L24
|
|
||||||
- https://github.com/tmccombs/tls-listener/blob/main/examples/tls_config/mod.rs
|
|
||||||
|
|
||||||
- logging
|
|
@ -1,6 +1,5 @@
|
|||||||
pub mod manager;
|
pub mod manager;
|
||||||
pub mod store;
|
pub mod store;
|
||||||
|
|
||||||
pub type PrivateKey = openssl::pkey::PKey<openssl::pkey::Private>;
|
pub type AccountKey = openssl::pkey::PKey<openssl::pkey::Private>;
|
||||||
|
|
||||||
pub type Certificate = openssl::x509::X509;
|
pub type Certificate = openssl::x509::X509;
|
||||||
|
@ -46,11 +46,8 @@ where
|
|||||||
.await
|
.await
|
||||||
.map_unexpected()?;
|
.map_unexpected()?;
|
||||||
|
|
||||||
let mut contact = String::from("mailto:");
|
|
||||||
contact.push_str(contact_email);
|
|
||||||
|
|
||||||
let mut builder = acme2::AccountBuilder::new(dir);
|
let mut builder = acme2::AccountBuilder::new(dir);
|
||||||
builder.contact(vec![contact]);
|
builder.contact(vec![contact_email.to_string()]);
|
||||||
builder.terms_of_service_agreed(true);
|
builder.terms_of_service_agreed(true);
|
||||||
|
|
||||||
match store.get_account_key() {
|
match store.get_account_key() {
|
||||||
@ -81,26 +78,6 @@ where
|
|||||||
|
|
||||||
fn sync_domain(&self, domain: domain::Name) -> Self::SyncDomainFuture<'_> {
|
fn sync_domain(&self, domain: domain::Name) -> Self::SyncDomainFuture<'_> {
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
// 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 cert_with_soonest_not_after = cert
|
|
||||||
.into_iter()
|
|
||||||
.reduce(|a, b| if a.not_after() < b.not_after() { a } else { b })
|
|
||||||
.ok_or(error::Unexpected::from(
|
|
||||||
"expected there to be more than one cert",
|
|
||||||
))?;
|
|
||||||
|
|
||||||
if thirty_days < cert_with_soonest_not_after.not_after() {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
println!("fetching a new certificate for domain {}", domain.as_str());
|
|
||||||
let mut builder = acme2::OrderBuilder::new(self.account.clone());
|
let mut builder = acme2::OrderBuilder::new(self.account.clone());
|
||||||
builder.add_dns_identifier(domain.as_str().to_string());
|
builder.add_dns_identifier(domain.as_str().to_string());
|
||||||
let order = builder.build().await.map_unexpected()?;
|
let order = builder.build().await.map_unexpected()?;
|
||||||
@ -129,27 +106,18 @@ where
|
|||||||
// At this point the manager is prepared to serve the challenge key via the
|
// 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
|
// `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
|
// server, with this domain pointing at it, which is prepared to serve that
|
||||||
// challenge token/key under the `/.well-known/acme-challenge` path. The
|
// challenge token/key under the `/.well-known/` path. The `validate()` call below
|
||||||
// `validate()` call below will instigate the acme server to make this check, and
|
// will instigate the acme server to make this check, and block until it succeeds.
|
||||||
// block until it succeeds.
|
|
||||||
|
|
||||||
println!(
|
|
||||||
"waiting for ACME challenge to be validated for domain {}",
|
|
||||||
domain.as_str(),
|
|
||||||
);
|
|
||||||
let challenge = challenge.validate().await.map_unexpected()?;
|
let challenge = challenge.validate().await.map_unexpected()?;
|
||||||
|
|
||||||
// Poll the challenge every 5 seconds until it is in either the
|
// Poll the challenge every 5 seconds until it is in either the
|
||||||
// `valid` or `invalid` state.
|
// `valid` or `invalid` state.
|
||||||
let challenge_res = challenge.wait_done(time::Duration::from_secs(5), 3).await;
|
let challenge = challenge
|
||||||
|
.wait_done(time::Duration::from_secs(5), 3)
|
||||||
// no matter what the result is, clean up the challenge key
|
.await
|
||||||
self.store
|
|
||||||
.del_http01_challenge_key(&challenge_token)
|
|
||||||
.map_unexpected()?;
|
.map_unexpected()?;
|
||||||
|
|
||||||
let challenge = challenge_res.map_unexpected()?;
|
|
||||||
|
|
||||||
if challenge.status != acme2::ChallengeStatus::Valid {
|
if challenge.status != acme2::ChallengeStatus::Valid {
|
||||||
return Err(error::Unexpected::from(
|
return Err(error::Unexpected::from(
|
||||||
format!(
|
format!(
|
||||||
@ -160,13 +128,12 @@ where
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.store
|
||||||
|
.del_http01_challenge_key(&challenge_token)
|
||||||
|
.map_unexpected()?;
|
||||||
|
|
||||||
// Poll the authorization every 5 seconds until it is in either the
|
// Poll the authorization every 5 seconds until it is in either the
|
||||||
// `valid` or `invalid` state.
|
// `valid` or `invalid` state.
|
||||||
println!(
|
|
||||||
"waiting for ACME authorization to be validated for domain {}",
|
|
||||||
domain.as_str(),
|
|
||||||
);
|
|
||||||
|
|
||||||
let authorization = auth
|
let authorization = auth
|
||||||
.wait_done(time::Duration::from_secs(5), 3)
|
.wait_done(time::Duration::from_secs(5), 3)
|
||||||
.await
|
.await
|
||||||
@ -185,11 +152,6 @@ where
|
|||||||
|
|
||||||
// Poll the order every 5 seconds until it is in either the `ready` or `invalid` state.
|
// 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).
|
// 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
|
let order = order
|
||||||
.wait_ready(time::Duration::from_secs(5), 3)
|
.wait_ready(time::Duration::from_secs(5), 3)
|
||||||
.await
|
.await
|
||||||
@ -211,17 +173,13 @@ where
|
|||||||
// Create a certificate signing request for the order, and request
|
// Create a certificate signing request for the order, and request
|
||||||
// the certificate.
|
// the certificate.
|
||||||
let order = order
|
let order = order
|
||||||
.finalize(acme2::Csr::Automatic(pkey.clone()))
|
.finalize(acme2::Csr::Automatic(pkey))
|
||||||
.await
|
.await
|
||||||
.map_unexpected()?;
|
.map_unexpected()?;
|
||||||
|
|
||||||
// Poll the order every 5 seconds until it is in either the
|
// Poll the order every 5 seconds until it is in either the
|
||||||
// `valid` or `invalid` state. Valid means that the certificate
|
// `valid` or `invalid` state. Valid means that the certificate
|
||||||
// has been provisioned, and is now ready for download.
|
// 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
|
let order = order
|
||||||
.wait_done(time::Duration::from_secs(5), 3)
|
.wait_done(time::Duration::from_secs(5), 3)
|
||||||
.await
|
.await
|
||||||
@ -238,7 +196,6 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Download the certificate, and panic if it doesn't exist.
|
// Download the certificate, and panic if it doesn't exist.
|
||||||
println!("fetching certificate for domain {}", domain.as_str());
|
|
||||||
let cert =
|
let cert =
|
||||||
order
|
order
|
||||||
.certificate()
|
.certificate()
|
||||||
@ -258,11 +215,6 @@ where
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
println!("certificate for {} successfully retrieved", domain.as_str());
|
|
||||||
self.store
|
|
||||||
.set_certificate(domain.as_str(), &pkey, cert)
|
|
||||||
.map_unexpected()?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,11 @@
|
|||||||
use std::io::{Read, Write};
|
use std::io::{Read, Write};
|
||||||
use std::{fs, io, path, sync};
|
use std::{fs, io, path, sync};
|
||||||
|
|
||||||
use crate::domain::acme::{Certificate, PrivateKey};
|
use crate::domain::acme::{AccountKey, Certificate};
|
||||||
use crate::error;
|
use crate::error;
|
||||||
use crate::error::{MapUnexpected, ToUnexpected};
|
use crate::error::{MapUnexpected, ToUnexpected};
|
||||||
|
|
||||||
use hex::ToHex;
|
use hex::ToHex;
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
|
|
||||||
#[derive(thiserror::Error, Debug)]
|
#[derive(thiserror::Error, Debug)]
|
||||||
@ -38,8 +37,8 @@ pub enum GetCertificateError {
|
|||||||
|
|
||||||
#[mockall::automock]
|
#[mockall::automock]
|
||||||
pub trait Store {
|
pub trait Store {
|
||||||
fn set_account_key(&self, k: &PrivateKey) -> Result<(), error::Unexpected>;
|
fn set_account_key(&self, k: &AccountKey) -> Result<(), error::Unexpected>;
|
||||||
fn get_account_key(&self) -> Result<PrivateKey, GetAccountKeyError>;
|
fn get_account_key(&self) -> Result<AccountKey, GetAccountKeyError>;
|
||||||
|
|
||||||
fn set_http01_challenge_key(&self, token: &str, key: &str) -> Result<(), error::Unexpected>;
|
fn set_http01_challenge_key(&self, token: &str, key: &str) -> Result<(), error::Unexpected>;
|
||||||
fn get_http01_challenge_key(&self, token: &str) -> Result<String, GetHttp01ChallengeKeyError>;
|
fn get_http01_challenge_key(&self, token: &str) -> Result<String, GetHttp01ChallengeKeyError>;
|
||||||
@ -48,25 +47,15 @@ pub trait Store {
|
|||||||
fn set_certificate(
|
fn set_certificate(
|
||||||
&self,
|
&self,
|
||||||
domain: &str,
|
domain: &str,
|
||||||
key: &PrivateKey,
|
|
||||||
cert: Vec<Certificate>,
|
cert: Vec<Certificate>,
|
||||||
) -> Result<(), error::Unexpected>;
|
) -> Result<(), error::Unexpected>;
|
||||||
|
|
||||||
/// Returned vec is guaranteed to have len > 0
|
/// Returned vec is guaranteed to have len > 0
|
||||||
fn get_certificate(
|
fn get_certificate(&self, domain: &str) -> Result<Vec<Certificate>, GetCertificateError>;
|
||||||
&self,
|
|
||||||
domain: &str,
|
|
||||||
) -> Result<(PrivateKey, Vec<Certificate>), GetCertificateError>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait BoxedStore: Store + Send + Sync + Clone + 'static {}
|
pub trait BoxedStore: Store + Send + Sync + Clone + 'static {}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
struct StoredPKeyCert {
|
|
||||||
private_key_pem: String,
|
|
||||||
cert_pems: Vec<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct FSStore {
|
struct FSStore {
|
||||||
dir_path: path::PathBuf,
|
dir_path: path::PathBuf,
|
||||||
}
|
}
|
||||||
@ -97,24 +86,21 @@ impl FSStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn certificate_path(&self, domain: &str) -> path::PathBuf {
|
fn certificate_path(&self, domain: &str) -> path::PathBuf {
|
||||||
self.dir_path
|
self.dir_path.join("certificates").join(domain)
|
||||||
.join("certificates")
|
|
||||||
.join(domain)
|
|
||||||
.with_extension("json")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BoxedStore for sync::Arc<FSStore> {}
|
impl BoxedStore for sync::Arc<FSStore> {}
|
||||||
|
|
||||||
impl Store for sync::Arc<FSStore> {
|
impl Store for sync::Arc<FSStore> {
|
||||||
fn set_account_key(&self, k: &PrivateKey) -> Result<(), error::Unexpected> {
|
fn set_account_key(&self, k: &AccountKey) -> Result<(), error::Unexpected> {
|
||||||
let mut file = fs::File::create(self.account_key_path()).map_unexpected()?;
|
let mut file = fs::File::create(self.account_key_path()).map_unexpected()?;
|
||||||
let pem = k.private_key_to_pem_pkcs8().map_unexpected()?;
|
let pem = k.private_key_to_pem_pkcs8().map_unexpected()?;
|
||||||
file.write_all(&pem).map_unexpected()?;
|
file.write_all(&pem).map_unexpected()?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_account_key(&self) -> Result<PrivateKey, GetAccountKeyError> {
|
fn get_account_key(&self) -> Result<AccountKey, GetAccountKeyError> {
|
||||||
let mut file = fs::File::open(self.account_key_path()).map_err(|e| match e.kind() {
|
let mut file = fs::File::open(self.account_key_path()).map_err(|e| match e.kind() {
|
||||||
io::ErrorKind::NotFound => GetAccountKeyError::NotFound,
|
io::ErrorKind::NotFound => GetAccountKeyError::NotFound,
|
||||||
_ => e.to_unexpected().into(),
|
_ => e.to_unexpected().into(),
|
||||||
@ -123,7 +109,7 @@ impl Store for sync::Arc<FSStore> {
|
|||||||
let mut pem = Vec::<u8>::new();
|
let mut pem = Vec::<u8>::new();
|
||||||
file.read_to_end(&mut pem).map_unexpected()?;
|
file.read_to_end(&mut pem).map_unexpected()?;
|
||||||
|
|
||||||
let k = PrivateKey::private_key_from_pem(&pem).map_unexpected()?;
|
let k = AccountKey::private_key_from_pem(&pem).map_unexpected()?;
|
||||||
Ok(k)
|
Ok(k)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -154,50 +140,38 @@ impl Store for sync::Arc<FSStore> {
|
|||||||
fn set_certificate(
|
fn set_certificate(
|
||||||
&self,
|
&self,
|
||||||
domain: &str,
|
domain: &str,
|
||||||
key: &PrivateKey,
|
|
||||||
cert: Vec<Certificate>,
|
cert: Vec<Certificate>,
|
||||||
) -> Result<(), error::Unexpected> {
|
) -> Result<(), error::Unexpected> {
|
||||||
let to_store = StoredPKeyCert {
|
let cert: Vec<String> = cert
|
||||||
private_key_pem: String::from_utf8(key.private_key_to_pem_pkcs8().map_unexpected()?)
|
.into_iter()
|
||||||
.map_unexpected()?,
|
.map(|cert| {
|
||||||
cert_pems: cert
|
let cert_pem = cert.to_pem().map_unexpected()?;
|
||||||
.into_iter()
|
let cert_pem = String::from_utf8(cert_pem).map_unexpected()?;
|
||||||
.map(|cert| {
|
Ok::<String, error::Unexpected>(cert_pem)
|
||||||
let cert_pem = cert.to_pem().map_unexpected()?;
|
})
|
||||||
let cert_pem = String::from_utf8(cert_pem).map_unexpected()?;
|
.try_collect()?;
|
||||||
Ok::<String, error::Unexpected>(cert_pem)
|
|
||||||
})
|
|
||||||
.try_collect()?,
|
|
||||||
};
|
|
||||||
|
|
||||||
let cert_file = fs::File::create(self.certificate_path(domain)).map_unexpected()?;
|
let cert_file = fs::File::create(self.certificate_path(domain)).map_unexpected()?;
|
||||||
|
|
||||||
serde_json::to_writer(cert_file, &to_store).map_unexpected()?;
|
serde_json::to_writer(cert_file, &cert).map_unexpected()?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_certificate(
|
fn get_certificate(&self, domain: &str) -> Result<Vec<Certificate>, GetCertificateError> {
|
||||||
&self,
|
|
||||||
domain: &str,
|
|
||||||
) -> Result<(PrivateKey, Vec<Certificate>), GetCertificateError> {
|
|
||||||
let file = fs::File::open(self.certificate_path(domain)).map_err(|e| match e.kind() {
|
let file = fs::File::open(self.certificate_path(domain)).map_err(|e| match e.kind() {
|
||||||
io::ErrorKind::NotFound => GetCertificateError::NotFound,
|
io::ErrorKind::NotFound => GetCertificateError::NotFound,
|
||||||
_ => e.to_unexpected().into(),
|
_ => e.to_unexpected().into(),
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let stored: StoredPKeyCert = serde_json::from_reader(file).map_unexpected()?;
|
let cert: Vec<String> = serde_json::from_reader(file).map_unexpected()?;
|
||||||
|
|
||||||
let key =
|
let cert: Vec<Certificate> = cert
|
||||||
PrivateKey::private_key_from_pem(stored.private_key_pem.as_bytes()).map_unexpected()?;
|
|
||||||
|
|
||||||
let cert: Vec<Certificate> = stored
|
|
||||||
.cert_pems
|
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|cert| openssl::x509::X509::from_pem(cert.as_bytes()).map_unexpected())
|
.map(|cert| openssl::x509::X509::from_pem(cert.as_bytes()).map_unexpected())
|
||||||
.try_collect()?;
|
.try_collect()?;
|
||||||
|
|
||||||
Ok((key, cert))
|
Ok(cert)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -213,7 +187,7 @@ mod tests {
|
|||||||
|
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
store.get_account_key(),
|
store.get_account_key(),
|
||||||
Err::<PrivateKey, GetAccountKeyError>(GetAccountKeyError::NotFound)
|
Err::<AccountKey, GetAccountKeyError>(GetAccountKeyError::NotFound)
|
||||||
));
|
));
|
||||||
|
|
||||||
let k = acme2::gen_rsa_private_key(4096).expect("private key generated");
|
let k = acme2::gen_rsa_private_key(4096).expect("private key generated");
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::str::FromStr;
|
|
||||||
use std::{fs, io, sync};
|
use std::{fs, io, sync};
|
||||||
|
|
||||||
use crate::error::{MapUnexpected, ToUnexpected};
|
use crate::error::{MapUnexpected, ToUnexpected};
|
||||||
@ -38,14 +37,10 @@ pub enum SetError {
|
|||||||
Unexpected(#[from] error::Unexpected),
|
Unexpected(#[from] error::Unexpected),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Used in the return from all_domains from Store.
|
|
||||||
pub type AllDomainsResult<T> = Result<T, error::Unexpected>;
|
|
||||||
|
|
||||||
#[mockall::automock]
|
#[mockall::automock]
|
||||||
pub trait Store {
|
pub trait Store {
|
||||||
fn get(&self, domain: &domain::Name) -> Result<Config, GetError>;
|
fn get(&self, domain: &domain::Name) -> Result<Config, GetError>;
|
||||||
fn set(&self, domain: &domain::Name, config: &Config) -> Result<(), SetError>;
|
fn set(&self, domain: &domain::Name, config: &Config) -> Result<(), SetError>;
|
||||||
fn all_domains(&self) -> AllDomainsResult<Vec<AllDomainsResult<domain::Name>>>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait BoxedStore: Store + Send + Sync + Clone {}
|
pub trait BoxedStore: Store + Send + Sync + Clone {}
|
||||||
@ -94,22 +89,6 @@ impl Store for sync::Arc<FSStore> {
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn all_domains(&self) -> AllDomainsResult<Vec<AllDomainsResult<domain::Name>>> {
|
|
||||||
Ok(fs::read_dir(&self.dir_path)
|
|
||||||
.map_unexpected()?
|
|
||||||
.map(
|
|
||||||
|dir_entry_res: io::Result<fs::DirEntry>| -> AllDomainsResult<domain::Name> {
|
|
||||||
let domain = dir_entry_res.map_unexpected()?.file_name();
|
|
||||||
let domain = domain.to_str().ok_or_else(|| {
|
|
||||||
error::Unexpected::from("couldn't convert os string to &str")
|
|
||||||
})?;
|
|
||||||
|
|
||||||
Ok(domain::Name::from_str(domain).map_unexpected()?)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.collect())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
@ -115,8 +115,6 @@ impl From<config::SetError> for SyncWithConfigError {
|
|||||||
|
|
||||||
pub type GetAcmeHttp01ChallengeKeyError = acme::manager::GetHttp01ChallengeKeyError;
|
pub type GetAcmeHttp01ChallengeKeyError = acme::manager::GetHttp01ChallengeKeyError;
|
||||||
|
|
||||||
pub type AllDomainsResult<T> = config::AllDomainsResult<T>;
|
|
||||||
|
|
||||||
#[mockall::automock(
|
#[mockall::automock(
|
||||||
type Origin=origin::MockOrigin;
|
type Origin=origin::MockOrigin;
|
||||||
type SyncWithConfigFuture=future::Ready<Result<(), SyncWithConfigError>>;
|
type SyncWithConfigFuture=future::Ready<Result<(), SyncWithConfigError>>;
|
||||||
@ -155,8 +153,6 @@ pub trait Manager {
|
|||||||
&self,
|
&self,
|
||||||
token: &str,
|
token: &str,
|
||||||
) -> Result<String, GetAcmeHttp01ChallengeKeyError>;
|
) -> Result<String, GetAcmeHttp01ChallengeKeyError>;
|
||||||
|
|
||||||
fn all_domains(&self) -> AllDomainsResult<Vec<AllDomainsResult<domain::Name>>>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait BoxedManager: Manager + Send + Sync + Clone {}
|
pub trait BoxedManager: Manager + Send + Sync + Clone {}
|
||||||
@ -287,8 +283,4 @@ where
|
|||||||
|
|
||||||
Err(GetAcmeHttp01ChallengeKeyError::NotFound)
|
Err(GetAcmeHttp01ChallengeKeyError::NotFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn all_domains(&self) -> AllDomainsResult<Vec<AllDomainsResult<domain::Name>>> {
|
|
||||||
self.domain_config_store.all_domains()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
104
src/main.rs
104
src/main.rs
@ -1,7 +1,6 @@
|
|||||||
#![feature(result_option_inspect)]
|
#![feature(result_option_inspect)]
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use futures::stream::futures_unordered::FuturesUnordered;
|
|
||||||
use futures::stream::StreamExt;
|
use futures::stream::StreamExt;
|
||||||
use signal_hook_tokio::Signals;
|
use signal_hook_tokio::Signals;
|
||||||
use tokio::select;
|
use tokio::select;
|
||||||
@ -21,7 +20,7 @@ use domiply::domain::manager::Manager;
|
|||||||
#[command(about = "A domiply to another dimension")]
|
#[command(about = "A domiply to another dimension")]
|
||||||
struct Cli {
|
struct Cli {
|
||||||
#[arg(long, required = true, env = "DOMIPLY_HTTP_DOMAIN")]
|
#[arg(long, required = true, env = "DOMIPLY_HTTP_DOMAIN")]
|
||||||
http_domain: domiply::domain::Name,
|
http_domain: String,
|
||||||
|
|
||||||
#[arg(long, default_value_t = SocketAddr::from_str("[::]:3030").unwrap(), env = "DOMIPLY_HTTP_LISTEN_ADDR")]
|
#[arg(long, default_value_t = SocketAddr::from_str("[::]:3030").unwrap(), env = "DOMIPLY_HTTP_LISTEN_ADDR")]
|
||||||
http_listen_addr: SocketAddr,
|
http_listen_addr: SocketAddr,
|
||||||
@ -29,8 +28,7 @@ struct Cli {
|
|||||||
#[arg(
|
#[arg(
|
||||||
long,
|
long,
|
||||||
help = "E.g. '[::]:443', if given then SSL certs will automatically be retrieved for all domains using LetsEncrypt",
|
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<SocketAddr>,
|
https_listen_addr: Option<SocketAddr>,
|
||||||
|
|
||||||
@ -52,15 +50,13 @@ struct Cli {
|
|||||||
#[arg(long, required = true, env = "DOMIPLY_DOMAIN_ACME_STORE_DIR_PATH")]
|
#[arg(long, required = true, env = "DOMIPLY_DOMAIN_ACME_STORE_DIR_PATH")]
|
||||||
domain_acme_store_dir_path: path::PathBuf,
|
domain_acme_store_dir_path: path::PathBuf,
|
||||||
|
|
||||||
#[arg(long, env = "DOMIPLY_DOMAIN_ACME_CONTACT_EMAIL")]
|
#[arg(long, required = true, env = "DOMIPLY_DOMAIN_ACME_CONTACT_EMAIL")]
|
||||||
domain_acme_contact_email: Option<String>,
|
domain_acme_contact_email: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let config = Cli::parse();
|
let config = Cli::parse();
|
||||||
|
|
||||||
let mut wait_group = FuturesUnordered::new();
|
|
||||||
|
|
||||||
let tokio_runtime = std::sync::Arc::new(
|
let tokio_runtime = std::sync::Arc::new(
|
||||||
tokio::runtime::Builder::new_multi_thread()
|
tokio::runtime::Builder::new_multi_thread()
|
||||||
.enable_all()
|
.enable_all()
|
||||||
@ -106,14 +102,13 @@ fn main() {
|
|||||||
domiply::domain::acme::store::new(&config.domain_acme_store_dir_path)
|
domiply::domain::acme::store::new(&config.domain_acme_store_dir_path)
|
||||||
.expect("domain acme store initialized");
|
.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 {
|
let domain_acme_manager = tokio_runtime.block_on(async {
|
||||||
domiply::domain::acme::manager::new(domain_acme_store, &domain_acme_contact_email)
|
domiply::domain::acme::manager::new(
|
||||||
.await
|
domain_acme_store,
|
||||||
.expect("domain acme manager initialized")
|
&config.domain_acme_contact_email,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("domain acme manager initialized")
|
||||||
});
|
});
|
||||||
|
|
||||||
Some(domain_acme_manager)
|
Some(domain_acme_manager)
|
||||||
@ -126,7 +121,7 @@ fn main() {
|
|||||||
domain_acme_manager.clone(),
|
domain_acme_manager.clone(),
|
||||||
);
|
);
|
||||||
|
|
||||||
wait_group.push({
|
let origin_syncer_handler = {
|
||||||
let manager = manager.clone();
|
let manager = manager.clone();
|
||||||
let canceller = canceller.clone();
|
let canceller = canceller.clone();
|
||||||
|
|
||||||
@ -155,10 +150,10 @@ fn main() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
});
|
};
|
||||||
|
|
||||||
let service = domiply::service::new(
|
let service = domiply::service::new(
|
||||||
manager.clone(),
|
manager,
|
||||||
config.domain_checker_target_a,
|
config.domain_checker_target_a,
|
||||||
config.passphrase,
|
config.passphrase,
|
||||||
config.http_domain.clone(),
|
config.http_domain.clone(),
|
||||||
@ -179,18 +174,13 @@ fn main() {
|
|||||||
async move { Ok::<_, Infallible>(service) }
|
async move { Ok::<_, Infallible>(service) }
|
||||||
});
|
});
|
||||||
|
|
||||||
wait_group.push({
|
let server_handler = {
|
||||||
let http_domain = config.http_domain.clone();
|
let http_domain = config.http_domain.clone();
|
||||||
let canceller = canceller.clone();
|
|
||||||
|
|
||||||
tokio_runtime.spawn(async move {
|
tokio_runtime.spawn(async move {
|
||||||
let addr = config.http_listen_addr;
|
let addr = config.http_listen_addr;
|
||||||
|
|
||||||
println!(
|
println!("Listening on http://{}:{}", http_domain, addr.port());
|
||||||
"Listening on http://{}:{}",
|
|
||||||
http_domain.as_str(),
|
|
||||||
addr.port()
|
|
||||||
);
|
|
||||||
let server = hyper::Server::bind(&addr).serve(make_service);
|
let server = hyper::Server::bind(&addr).serve(make_service);
|
||||||
|
|
||||||
let graceful = server.with_graceful_shutdown(async {
|
let graceful = server.with_graceful_shutdown(async {
|
||||||
@ -201,63 +191,27 @@ fn main() {
|
|||||||
panic!("server error: {}", e);
|
panic!("server error: {}", e);
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
});
|
};
|
||||||
|
|
||||||
// if there's an acme manager then it means that https is enabled, and we should ensure that
|
// if there's an acme manager then it means that https is enabled, and we should ensure that
|
||||||
// the http domain for domiply itself has a valid certificate.
|
// the http domain for domiply itself has a valid certificate.
|
||||||
if let Some(domain_acme_manager) = domain_acme_manager {
|
if let Some(domain_acme_manager) = domain_acme_manager {
|
||||||
let manager = manager.clone();
|
let domain = domiply::domain::Name::from_str(&config.http_domain)
|
||||||
let canceller = canceller.clone();
|
.expect("--http-domain parses as a domain");
|
||||||
let http_domain = config.http_domain.clone();
|
|
||||||
|
|
||||||
// Periodically refresh all domain certs
|
tokio_runtime.spawn(async move {
|
||||||
wait_group.push(tokio_runtime.spawn(async move {
|
_ = domain_acme_manager
|
||||||
let mut interval = time::interval(time::Duration::from_secs(60 * 60));
|
.sync_domain(domain.clone())
|
||||||
|
.await
|
||||||
loop {
|
.inspect_err(|err| {
|
||||||
select! {
|
println!("Error while getting cert for {}: {err}", domain.as_str())
|
||||||
_ = interval.tick() => (),
|
});
|
||||||
_ = canceller.cancelled() => return,
|
});
|
||||||
}
|
|
||||||
|
|
||||||
_ = domain_acme_manager
|
|
||||||
.sync_domain(http_domain.clone())
|
|
||||||
.await
|
|
||||||
.inspect_err(|err| {
|
|
||||||
println!(
|
|
||||||
"Error while getting cert for {}: {err}",
|
|
||||||
http_domain.as_str()
|
|
||||||
)
|
|
||||||
});
|
|
||||||
|
|
||||||
let domains_iter = manager.all_domains();
|
|
||||||
|
|
||||||
if let Err(err) = domains_iter {
|
|
||||||
println!("Got error calling all_domains: {err}");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
for domain in domains_iter.unwrap().into_iter() {
|
|
||||||
match domain {
|
|
||||||
Ok(domain) => {
|
|
||||||
let _ = domain_acme_manager
|
|
||||||
.sync_domain(domain.clone())
|
|
||||||
.await
|
|
||||||
.inspect_err(|err| {
|
|
||||||
println!(
|
|
||||||
"Error while getting cert for {}: {err}",
|
|
||||||
domain.as_str(),
|
|
||||||
)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Err(err) => println!("Error iterating through domains: {err}"),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tokio_runtime.block_on(async { while let Some(_) = wait_group.next().await {} });
|
tokio_runtime
|
||||||
|
.block_on(async { futures::try_join!(origin_syncer_handler, server_handler) })
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
println!("Graceful shutdown complete");
|
println!("Graceful shutdown complete");
|
||||||
}
|
}
|
||||||
|
@ -23,7 +23,7 @@ where
|
|||||||
domain_manager: DomainManager,
|
domain_manager: DomainManager,
|
||||||
target_a: net::Ipv4Addr,
|
target_a: net::Ipv4Addr,
|
||||||
passphrase: String,
|
passphrase: String,
|
||||||
http_domain: domain::Name,
|
http_domain: String,
|
||||||
handlebars: handlebars::Handlebars<'svc>,
|
handlebars: handlebars::Handlebars<'svc>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -31,7 +31,7 @@ pub fn new<'svc, DomainManager>(
|
|||||||
domain_manager: DomainManager,
|
domain_manager: DomainManager,
|
||||||
target_a: net::Ipv4Addr,
|
target_a: net::Ipv4Addr,
|
||||||
passphrase: String,
|
passphrase: String,
|
||||||
http_domain: domain::Name,
|
http_domain: String,
|
||||||
) -> Service<'svc, DomainManager>
|
) -> Service<'svc, DomainManager>
|
||||||
where
|
where
|
||||||
DomainManager: domain::manager::BoxedManager,
|
DomainManager: domain::manager::BoxedManager,
|
||||||
@ -324,8 +324,8 @@ where
|
|||||||
.map(strip_port),
|
.map(strip_port),
|
||||||
req.uri().host().map(strip_port),
|
req.uri().host().map(strip_port),
|
||||||
) {
|
) {
|
||||||
(Some(h), _) if h != svc.http_domain.as_str() => Some(h),
|
(Some(h), _) if h != svc.http_domain => Some(h),
|
||||||
(_, Some(h)) if h != svc.http_domain.as_str() => Some(h),
|
(_, Some(h)) if h != svc.http_domain => Some(h),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
.and_then(|h| domain::Name::from_str(h).ok());
|
.and_then(|h| domain::Name::from_str(h).ok());
|
||||||
@ -341,8 +341,8 @@ where
|
|||||||
return svc.render(200, path, ());
|
return svc.render(200, path, ());
|
||||||
}
|
}
|
||||||
|
|
||||||
if method == Method::GET && path.starts_with("/.well-known/acme-challenge/") {
|
if method == Method::GET && path.starts_with("/.well-known/") {
|
||||||
let token = path.trim_start_matches("/.well-known/acme-challenge/");
|
let token = path.trim_start_matches("/.well-known/");
|
||||||
|
|
||||||
if let Ok(key) = svc.domain_manager.get_acme_http01_challenge_key(token) {
|
if let Ok(key) = svc.domain_manager.get_acme_http01_challenge_key(token) {
|
||||||
let body: hyper::Body = key.into();
|
let body: hyper::Body = key.into();
|
||||||
|
Loading…
Reference in New Issue
Block a user