Compare commits

...

3 Commits

Author SHA1 Message Date
Brian Picciano
c5659ecc4e Update readme and index 2023-05-20 15:03:11 +02:00
Brian Picciano
9c2bd4e49a cleaned up https parameter handling a bit 2023-05-20 14:51:36 +02:00
Brian Picciano
0fd832efdd clippy suggestions 2023-05-20 14:34:45 +02:00
10 changed files with 130 additions and 47 deletions

View File

@ -7,6 +7,78 @@ their DNS server.
[Demo which may or may not be live](https://domiply.mediocregopher.com) [Demo which may or may not be live](https://domiply.mediocregopher.com)
## Build
Domiply uses nix flakes for building and setting up the development environment.
In order to create a release binary:
```
nix build
```
A statically compiled binary will be placed in the `result` directory.
## Configuration
Domiply is configured via command-line arguments or environment variables:
```
--http-domain <HTTP_DOMAIN>
[env: DOMIPLY_HTTP_DOMAIN=]
--http-listen-addr <HTTP_LISTEN_ADDR>
[env: DOMIPLY_HTTP_LISTEN_ADDR=] [default: [::]:3030]
--https-listen-addr <HTTPS_LISTEN_ADDR>
E.g. '[::]:443', if given then SSL certs will automatically be retrieved for all domains using LetsEncrypt [env: DOMIPLY_HTTPS_LISTEN_ADDR=]
--passphrase <PASSPHRASE>
[env: DOMIPLY_PASSPHRASE=]
--origin-store-git-dir-path <ORIGIN_STORE_GIT_DIR_PATH>
[env: DOMIPLY_ORIGIN_STORE_GIT_DIR_PATH=]
--domain-checker-target-a <DOMAIN_CHECKER_TARGET_A>
[env: DOMIPLY_DOMAIN_CHECKER_TARGET_A=]
--domain-checker-resolver-addr <DOMAIN_CHECKER_RESOLVER_ADDR>
[env: DOMIPLY_DOMAIN_CHECKER_RESOLVER_ADDR=] [default: 1.1.1.1:53]
--domain-config-store-dir-path <DOMAIN_CONFIG_STORE_DIR_PATH>
[env: DOMIPLY_DOMAIN_CONFIG_STORE_DIR_PATH=]
--domain-acme-store-dir-path <DOMAIN_ACME_STORE_DIR_PATH>
[env: DOMIPLY_DOMAIN_ACME_STORE_DIR_PATH=]
--domain-acme-contact-email <DOMAIN_ACME_CONTACT_EMAIL>
[env: DOMIPLY_DOMAIN_ACME_CONTACT_EMAIL=]
-h, --help
Print help
-V, --version
Print version
```
### HTTPS Support
Domiply will automatically handle setting up HTTPS via LetsEncrypt for both the
domiply frontend site and all domains which it has been configured to serve.
By default HTTPS is not enabled, but can be easily enabled by setting the
following arguments:
```
--https-listen-addr='[::]:443'
--domain-acme-contact-email='foo@example.com'
--domain-acme-store-dir-path='/some/secure/directory'
```
The contact email can be anything, it doesn't have to be real. The store
directory will have all SSL private keys written to it, and so should be
secured as best as possible.
## Development ## Development
Domiply uses nix flakes for building and setting up the development environment. Domiply uses nix flakes for building and setting up the development environment.
@ -19,12 +91,6 @@ nix develop
Within the shell which opens you can do `cargo run` to start a local instance. Within the shell which opens you can do `cargo run` to start a local instance.
In order to create a release binary:
```
nix build
```
## Roadmap ## Roadmap
Check out the `src/service/http_tpl/index.html` file for the current roadmap. Check out the `src/service/http_tpl/index.html` file for the current roadmap.

1
TODO
View File

@ -1,3 +1,4 @@
- logging - logging
- expect statements (pretend it's "expected", not "expect") - expect statements (pretend it's "expected", not "expect")
- map_unexpected annotation string - map_unexpected annotation string
- clean up main a lot

View File

@ -34,7 +34,7 @@ impl TryFrom<&Certificate> for openssl::x509::X509 {
type Error = openssl::error::ErrorStack; type Error = openssl::error::ErrorStack;
fn try_from(c: &Certificate) -> Result<Self, Self::Error> { fn try_from(c: &Certificate) -> Result<Self, Self::Error> {
Ok(openssl::x509::X509::from_der(&c.0)?) openssl::x509::X509::from_der(&c.0)
} }
} }

View File

@ -4,7 +4,7 @@ use crate::domain::{self, acme};
use crate::error; use crate::error;
use crate::error::{MapUnexpected, ToUnexpected}; use crate::error::{MapUnexpected, ToUnexpected};
const LETS_ENCRYPT_URL: &'static str = "https://acme-v02.api.letsencrypt.org/directory"; const LETS_ENCRYPT_URL: &str = "https://acme-v02.api.letsencrypt.org/directory";
pub type GetHttp01ChallengeKeyError = acme::store::GetHttp01ChallengeKeyError; pub type GetHttp01ChallengeKeyError = acme::store::GetHttp01ChallengeKeyError;
@ -149,7 +149,7 @@ where
// no matter what the result is, clean up the challenge key // no matter what the result is, clean up the challenge key
self.store self.store
.del_http01_challenge_key(&challenge_token) .del_http01_challenge_key(challenge_token)
.map_unexpected()?; .map_unexpected()?;
let challenge = challenge_res.map_unexpected()?; let challenge = challenge_res.map_unexpected()?;

View File

@ -9,6 +9,7 @@ use serde_with::{DeserializeFromStr, SerializeDisplay};
pub struct PrivateKey(Vec<u8>); pub struct PrivateKey(Vec<u8>);
impl PrivateKey { impl PrivateKey {
#[allow(clippy::new_without_default)]
pub fn new() -> PrivateKey { pub fn new() -> PrivateKey {
acme2::gen_rsa_private_key(4096) acme2::gen_rsa_private_key(4096)
.expect("RSA private key generated") .expect("RSA private key generated")
@ -44,7 +45,7 @@ impl TryFrom<&PrivateKey> for openssl::pkey::PKey<openssl::pkey::Private> {
type Error = openssl::error::ErrorStack; type Error = openssl::error::ErrorStack;
fn try_from(k: &PrivateKey) -> Result<Self, Self::Error> { fn try_from(k: &PrivateKey) -> Result<Self, Self::Error> {
Ok(openssl::pkey::PKey::private_key_from_der(&k.0)?) openssl::pkey::PKey::private_key_from_der(&k.0)
} }
} }

View File

@ -164,7 +164,7 @@ impl Store for BoxedFSStore {
) -> Result<(), error::Unexpected> { ) -> Result<(), error::Unexpected> {
let to_store = StoredPKeyCert { let to_store = StoredPKeyCert {
private_key: key, private_key: key,
cert: cert, cert,
}; };
let cert_file = fs::File::create(self.certificate_path(domain)).map_unexpected()?; let cert_file = fs::File::create(self.certificate_path(domain)).map_unexpected()?;
@ -194,11 +194,7 @@ impl rustls::server::ResolvesServerCert for BoxedFSStore {
&self, &self,
client_hello: rustls::server::ClientHello<'_>, client_hello: rustls::server::ClientHello<'_>,
) -> Option<sync::Arc<rustls::sign::CertifiedKey>> { ) -> Option<sync::Arc<rustls::sign::CertifiedKey>> {
let domain = if let Some(domain) = client_hello.server_name() { let domain = client_hello.server_name()?;
domain
} else {
return None;
};
match self.get_certificate(domain) { match self.get_certificate(domain) {
Err(GetCertificateError::NotFound) => Ok(None), Err(GetCertificateError::NotFound) => Ok(None),
@ -208,7 +204,7 @@ impl rustls::server::ResolvesServerCert for BoxedFSStore {
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 {
cert: cert.into_iter().map(|cert| cert.into()).collect(), cert: cert.into_iter().map(|cert| cert.into()).collect(),
key: key, key,
ocsp: None, ocsp: None,
sct_list: None, sct_list: None,
}))), }))),

View File

@ -105,7 +105,7 @@ impl Store for sync::Arc<FSStore> {
error::Unexpected::from("couldn't convert os string to &str") error::Unexpected::from("couldn't convert os string to &str")
})?; })?;
Ok(domain::Name::from_str(domain).map_unexpected()?) domain::Name::from_str(domain).map_unexpected()
}, },
) )
.collect()) .collect())

View File

@ -188,7 +188,7 @@ where
origin_store, origin_store,
domain_config_store, domain_config_store,
domain_checker, domain_checker,
acme_manager: acme_manager, acme_manager,
}) })
} }
@ -261,7 +261,7 @@ where
Ok(Box::from(iter.filter_map(|descr| { Ok(Box::from(iter.filter_map(|descr| {
if let Err(err) = descr { if let Err(err) = descr {
return Some((None, err.to_unexpected().into())); return Some((None, err.to_unexpected()));
} }
let descr = descr.unwrap(); let descr = descr.unwrap();
@ -270,7 +270,7 @@ where
.origin_store .origin_store
.sync(descr.clone(), origin::store::Limits {}) .sync(descr.clone(), origin::store::Limits {})
{ {
return Some((Some(descr), err.to_unexpected().into())); return Some((Some(descr), err.to_unexpected()));
} }
None None

View File

@ -30,7 +30,8 @@ struct Cli {
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" requires = "domain_acme_contact_email",
requires = "domain_acme_store_dir_path"
)] )]
https_listen_addr: Option<SocketAddr>, https_listen_addr: Option<SocketAddr>,
@ -49,13 +50,24 @@ struct Cli {
#[arg(long, required = true, env = "DOMIPLY_DOMAIN_CONFIG_STORE_DIR_PATH")] #[arg(long, required = true, env = "DOMIPLY_DOMAIN_CONFIG_STORE_DIR_PATH")]
domain_config_store_dir_path: path::PathBuf, domain_config_store_dir_path: path::PathBuf,
#[arg(long, required = true, env = "DOMIPLY_DOMAIN_ACME_STORE_DIR_PATH")] #[arg(long, env = "DOMIPLY_DOMAIN_ACME_STORE_DIR_PATH")]
domain_acme_store_dir_path: path::PathBuf, domain_acme_store_dir_path: Option<path::PathBuf>,
#[arg(long, env = "DOMIPLY_DOMAIN_ACME_CONTACT_EMAIL")] #[arg(long, env = "DOMIPLY_DOMAIN_ACME_CONTACT_EMAIL")]
domain_acme_contact_email: Option<String>, domain_acme_contact_email: Option<String>,
} }
#[derive(Clone)]
struct HTTPSParams<DomainAcmeStore, DomainAcmeManager>
where
DomainAcmeStore: domiply::domain::acme::store::BoxedStore,
DomainAcmeManager: domiply::domain::acme::manager::BoxedManager,
{
https_listen_addr: SocketAddr,
domain_acme_store: DomainAcmeStore,
domain_acme_manager: DomainAcmeManager,
}
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
let config = Cli::parse(); let config = Cli::parse();
@ -95,9 +107,10 @@ async fn main() {
let domain_config_store = domiply::domain::config::new(&config.domain_config_store_dir_path) let domain_config_store = domiply::domain::config::new(&config.domain_config_store_dir_path)
.expect("domain config store initialized"); .expect("domain config store initialized");
let (domain_acme_store, domain_acme_manager) = if config.https_listen_addr.is_some() { let https_params = if let Some(https_listen_addr) = config.https_listen_addr {
let domain_acme_store = let domain_acme_store_dir_path = config.domain_acme_store_dir_path.unwrap();
domiply::domain::acme::store::new(&config.domain_acme_store_dir_path)
let domain_acme_store = domiply::domain::acme::store::new(&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 // if https_listen_addr is set then domain_acme_contact_email is required, see the Cli/clap
@ -111,20 +124,26 @@ async fn main() {
.await .await
.expect("domain acme manager initialized"); .expect("domain acme manager initialized");
(Some(domain_acme_store), Some(domain_acme_manager)) Some(HTTPSParams {
https_listen_addr,
domain_acme_store,
domain_acme_manager,
})
} else { } else {
(None, None) None
}; };
let manager = domiply::domain::manager::new( let domain_manager = domiply::domain::manager::new(
origin_store, origin_store,
domain_config_store, domain_config_store,
domain_checker, domain_checker,
domain_acme_manager.clone(), https_params
.as_ref()
.and_then(|p| Some(p.domain_acme_manager.clone())),
); );
wait_group.push({ wait_group.push({
let manager = manager.clone(); let domain_manager = domain_manager.clone();
let canceller = canceller.clone(); let canceller = canceller.clone();
tokio::spawn(async move { tokio::spawn(async move {
@ -136,7 +155,7 @@ async fn main() {
_ = canceller.cancelled() => return, _ = canceller.cancelled() => return,
} }
let errors_iter = manager.sync_all_origins(); let errors_iter = domain_manager.sync_all_origins();
if let Err(err) = errors_iter { if let Err(err) = errors_iter {
println!("Got error calling sync_all_origins: {err}"); println!("Got error calling sync_all_origins: {err}");
@ -155,7 +174,7 @@ async fn main() {
}); });
let service = domiply::service::new( let service = domiply::service::new(
manager.clone(), domain_manager.clone(),
config.domain_checker_target_a, config.domain_checker_target_a,
config.passphrase, config.passphrase,
config.http_domain.clone(), config.http_domain.clone(),
@ -200,13 +219,11 @@ async fn main() {
}) })
}); });
// if there's an acme manager then it means that https is enabled if let Some(https_params) = https_params {
if let (Some(domain_acme_store), Some(domain_acme_manager)) =
(domain_acme_store, domain_acme_manager)
{
// Periodically refresh all domain certs, including the http_domain passed in the Cli opts // Periodically refresh all domain certs, including the http_domain passed in the Cli opts
wait_group.push({ wait_group.push({
let manager = manager.clone(); let https_params = https_params.clone();
let domain_manager = domain_manager.clone();
let http_domain = config.http_domain.clone(); let http_domain = config.http_domain.clone();
let canceller = canceller.clone(); let canceller = canceller.clone();
@ -219,7 +236,8 @@ async fn main() {
_ = canceller.cancelled() => return, _ = canceller.cancelled() => return,
} }
_ = domain_acme_manager _ = https_params
.domain_acme_manager
.sync_domain(http_domain.clone()) .sync_domain(http_domain.clone())
.await .await
.inspect_err(|err| { .inspect_err(|err| {
@ -229,7 +247,7 @@ async fn main() {
) )
}); });
let domains_iter = manager.all_domains(); let domains_iter = domain_manager.all_domains();
if let Err(err) = domains_iter { if let Err(err) = domains_iter {
println!("Got error calling all_domains: {err}"); println!("Got error calling all_domains: {err}");
@ -239,7 +257,8 @@ async fn main() {
for domain in domains_iter.unwrap().into_iter() { for domain in domains_iter.unwrap().into_iter() {
match domain { match domain {
Ok(domain) => { Ok(domain) => {
let _ = domain_acme_manager let _ = https_params
.domain_acme_manager
.sync_domain(domain.clone()) .sync_domain(domain.clone())
.await .await
.inspect_err(|err| { .inspect_err(|err| {
@ -258,6 +277,7 @@ async fn main() {
// HTTPS server // HTTPS server
wait_group.push({ wait_group.push({
let https_params = https_params.clone();
let http_domain = config.http_domain.clone(); let http_domain = config.http_domain.clone();
let canceller = canceller.clone(); let canceller = canceller.clone();
let service = service.clone(); let service = service.clone();
@ -283,11 +303,11 @@ async fn main() {
.with_safe_default_protocol_versions() .with_safe_default_protocol_versions()
.unwrap() .unwrap()
.with_no_client_auth() .with_no_client_auth()
.with_cert_resolver(sync::Arc::from(domain_acme_store)), .with_cert_resolver(sync::Arc::from(https_params.domain_acme_store)),
) )
.into(); .into();
let addr = config.https_listen_addr.unwrap(); let addr = https_params.https_listen_addr;
let addr_incoming = hyper::server::conn::AddrIncoming::bind(&addr) let addr_incoming = hyper::server::conn::AddrIncoming::bind(&addr)
.expect("https listen socket created"); .expect("https listen socket created");
@ -312,7 +332,7 @@ async fn main() {
}) })
} }
while let Some(_) = wait_group.next().await {} while wait_group.next().await.is_some() {}
println!("Graceful shutdown complete"); println!("Graceful shutdown complete");
} }

View File

@ -48,7 +48,6 @@ planned but not yet implemented:</p>
<ul> <ul>
<li>Support for AAAA and CNAME records</li> <li>Support for AAAA and CNAME records</li>
<li>HTTPS support, with automatic certificate syncing via Let's Encrypt.</li>
<li> <li>
Support for more backends than just git repositories, including: Support for more backends than just git repositories, including:
<ul> <ul>