Compare commits
3 Commits
e29de0d29c
...
c5659ecc4e
Author | SHA1 | Date | |
---|---|---|---|
|
c5659ecc4e | ||
|
9c2bd4e49a | ||
|
0fd832efdd |
78
README.md
78
README.md
@ -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
1
TODO
@ -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
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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()?;
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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,
|
||||||
}))),
|
}))),
|
||||||
|
@ -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())
|
||||||
|
@ -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
|
||||||
|
68
src/main.rs
68
src/main.rs
@ -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");
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
Loading…
Reference in New Issue
Block a user