Compare commits
No commits in common. "644d2bab23381d5bcdb8c48480dce8f12ccda65e" and "f2374cded5e3a67d181c309522cc3649672af524" have entirely different histories.
644d2bab23
...
f2374cded5
12
.env.dev
12
.env.dev
@ -1,6 +1,6 @@
|
||||
export DOMANI_HTTP_DOMAIN=localhost
|
||||
export DOMANI_PASSPHRASE=foobar
|
||||
export DOMANI_ORIGIN_STORE_GIT_DIR_PATH=/tmp/domani_dev_env/origin/git
|
||||
export DOMANI_DOMAIN_CHECKER_TARGET_A=127.0.0.1
|
||||
export DOMANI_DOMAIN_CONFIG_STORE_DIR_PATH=/tmp/domani_dev_env/domain/config
|
||||
export DOMANI_DOMAIN_ACME_STORE_DIR_PATH=/tmp/domani_dev_env/domain/acme
|
||||
export DOMIPLY_HTTP_DOMAIN=localhost
|
||||
export DOMIPLY_PASSPHRASE=foobar
|
||||
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_CONFIG_STORE_DIR_PATH=/tmp/domiply_dev_env/domain/config
|
||||
export DOMIPLY_DOMAIN_ACME_STORE_DIR_PATH=/tmp/domiply_dev_env/domain/acme
|
||||
|
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -444,7 +444,7 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "domani"
|
||||
name = "domiply"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"acme2",
|
||||
|
@ -1,5 +1,5 @@
|
||||
[package]
|
||||
name = "domani"
|
||||
name = "domiply"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
|
36
README.md
36
README.md
@ -1,15 +1,15 @@
|
||||
# Domani
|
||||
# Domiply
|
||||
|
||||
Domani is a self-hosted rust service which connects a DNS hostname to a data
|
||||
Domiply is a self-hosted rust service which connects a DNS hostname to a data
|
||||
backend (e.g. a git repository), all with no account needed. The user only
|
||||
inputs their domain name, their desired backend, and then adds two entries to
|
||||
their DNS server.
|
||||
|
||||
[Demo which may or may not be live](https://domani.mediocregopher.com)
|
||||
[Demo which may or may not be live](https://domiply.mediocregopher.com)
|
||||
|
||||
## Build
|
||||
|
||||
Domani uses nix flakes for building and setting up the development environment.
|
||||
Domiply uses nix flakes for building and setting up the development environment.
|
||||
|
||||
In order to create a release binary:
|
||||
|
||||
@ -21,38 +21,38 @@ A statically compiled binary will be placed in the `result` directory.
|
||||
|
||||
## Configuration
|
||||
|
||||
Domani is configured via command-line arguments or environment variables:
|
||||
Domiply is configured via command-line arguments or environment variables:
|
||||
|
||||
```
|
||||
--http-domain <HTTP_DOMAIN>
|
||||
[env: DOMANI_HTTP_DOMAIN=]
|
||||
[env: DOMIPLY_HTTP_DOMAIN=]
|
||||
|
||||
--http-listen-addr <HTTP_LISTEN_ADDR>
|
||||
[env: DOMANI_HTTP_LISTEN_ADDR=] [default: [::]:3030]
|
||||
[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: DOMANI_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: DOMANI_PASSPHRASE=]
|
||||
[env: DOMIPLY_PASSPHRASE=]
|
||||
|
||||
--origin-store-git-dir-path <ORIGIN_STORE_GIT_DIR_PATH>
|
||||
[env: DOMANI_ORIGIN_STORE_GIT_DIR_PATH=]
|
||||
[env: DOMIPLY_ORIGIN_STORE_GIT_DIR_PATH=]
|
||||
|
||||
--domain-checker-target-a <DOMAIN_CHECKER_TARGET_A>
|
||||
[env: DOMANI_DOMAIN_CHECKER_TARGET_A=]
|
||||
[env: DOMIPLY_DOMAIN_CHECKER_TARGET_A=]
|
||||
|
||||
--domain-checker-resolver-addr <DOMAIN_CHECKER_RESOLVER_ADDR>
|
||||
[env: DOMANI_DOMAIN_CHECKER_RESOLVER_ADDR=] [default: 1.1.1.1:53]
|
||||
[env: DOMIPLY_DOMAIN_CHECKER_RESOLVER_ADDR=] [default: 1.1.1.1:53]
|
||||
|
||||
--domain-config-store-dir-path <DOMAIN_CONFIG_STORE_DIR_PATH>
|
||||
[env: DOMANI_DOMAIN_CONFIG_STORE_DIR_PATH=]
|
||||
[env: DOMIPLY_DOMAIN_CONFIG_STORE_DIR_PATH=]
|
||||
|
||||
--domain-acme-store-dir-path <DOMAIN_ACME_STORE_DIR_PATH>
|
||||
[env: DOMANI_DOMAIN_ACME_STORE_DIR_PATH=]
|
||||
[env: DOMIPLY_DOMAIN_ACME_STORE_DIR_PATH=]
|
||||
|
||||
--domain-acme-contact-email <DOMAIN_ACME_CONTACT_EMAIL>
|
||||
[env: DOMANI_DOMAIN_ACME_CONTACT_EMAIL=]
|
||||
[env: DOMIPLY_DOMAIN_ACME_CONTACT_EMAIL=]
|
||||
|
||||
-h, --help
|
||||
Print help
|
||||
@ -63,8 +63,8 @@ Domani is configured via command-line arguments or environment variables:
|
||||
|
||||
### HTTPS Support
|
||||
|
||||
Domani will automatically handle setting up HTTPS via LetsEncrypt for both the
|
||||
domani frontend site and all domains which it has been configured to serve.
|
||||
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:
|
||||
@ -81,7 +81,7 @@ secured as best as possible.
|
||||
|
||||
## Development
|
||||
|
||||
Domani uses nix flakes for building and setting up the development environment.
|
||||
Domiply uses nix flakes for building and setting up the development environment.
|
||||
In order to open a shell with all necessary tooling (expected rust toolchain
|
||||
versions, etc...) simply do:
|
||||
|
||||
|
3
TODO
3
TODO
@ -0,0 +1,3 @@
|
||||
- make domain_manager implement rusttls cert resolver
|
||||
- Try to switch from Arc to Box where possible
|
||||
- maybe build TaskSet into some kind of defer-like replacement
|
@ -1,4 +1,5 @@
|
||||
pub mod manager;
|
||||
pub mod resolver;
|
||||
pub mod store;
|
||||
|
||||
mod private_key;
|
||||
|
@ -1,13 +1,11 @@
|
||||
use std::{future, pin, sync, time};
|
||||
|
||||
use crate::domain::acme::{Certificate, PrivateKey};
|
||||
use crate::domain::{self, acme};
|
||||
use crate::error::unexpected::{self, Intoable, Mappable};
|
||||
|
||||
const LETS_ENCRYPT_URL: &str = "https://acme-v02.api.letsencrypt.org/directory";
|
||||
|
||||
pub type GetHttp01ChallengeKeyError = acme::store::GetHttp01ChallengeKeyError;
|
||||
pub type GetCertificateError = acme::store::GetCertificateError;
|
||||
|
||||
#[mockall::automock]
|
||||
pub trait Manager: Sync + Send {
|
||||
@ -16,23 +14,17 @@ pub trait Manager: Sync + Send {
|
||||
domain: domain::Name,
|
||||
) -> pin::Pin<Box<dyn future::Future<Output = Result<(), unexpected::Error>> + Send + 'mgr>>;
|
||||
fn get_http01_challenge_key(&self, token: &str) -> Result<String, GetHttp01ChallengeKeyError>;
|
||||
|
||||
/// Returned vec is guaranteed to have len > 0
|
||||
fn get_certificate(
|
||||
&self,
|
||||
domain: &str,
|
||||
) -> Result<(PrivateKey, Vec<Certificate>), GetCertificateError>;
|
||||
}
|
||||
|
||||
struct ManagerImpl {
|
||||
store: Box<dyn acme::store::Store>,
|
||||
store: sync::Arc<dyn acme::store::Store>,
|
||||
account: sync::Arc<acme2::Account>,
|
||||
}
|
||||
|
||||
pub async fn new(
|
||||
store: Box<dyn acme::store::Store>,
|
||||
store: sync::Arc<dyn acme::store::Store>,
|
||||
contact_email: &str,
|
||||
) -> Result<Box<dyn Manager>, unexpected::Error> {
|
||||
) -> Result<sync::Arc<dyn Manager>, unexpected::Error> {
|
||||
let dir = acme2::DirectoryBuilder::new(LETS_ENCRYPT_URL.to_string())
|
||||
.build()
|
||||
.await
|
||||
@ -61,7 +53,6 @@ pub async fn new(
|
||||
.build()
|
||||
.await
|
||||
.or_unexpected_while("building account")?;
|
||||
|
||||
let account_key: acme::PrivateKey = account
|
||||
.private_key()
|
||||
.as_ref()
|
||||
@ -72,7 +63,7 @@ pub async fn new(
|
||||
.set_account_key(&account_key)
|
||||
.or_unexpected_while("storing account key")?;
|
||||
|
||||
Ok(Box::new(ManagerImpl { store, account }))
|
||||
Ok(sync::Arc::new(ManagerImpl { store, account }))
|
||||
}
|
||||
|
||||
impl Manager for ManagerImpl {
|
||||
@ -291,12 +282,4 @@ impl Manager for ManagerImpl {
|
||||
fn get_http01_challenge_key(&self, token: &str) -> Result<String, GetHttp01ChallengeKeyError> {
|
||||
self.store.get_http01_challenge_key(token)
|
||||
}
|
||||
|
||||
/// Returned vec is guaranteed to have len > 0
|
||||
fn get_certificate(
|
||||
&self,
|
||||
domain: &str,
|
||||
) -> Result<(PrivateKey, Vec<Certificate>), GetCertificateError> {
|
||||
self.store.get_certificate(domain)
|
||||
}
|
||||
}
|
||||
|
44
src/domain/acme/resolver.rs
Normal file
44
src/domain/acme/resolver.rs
Normal file
@ -0,0 +1,44 @@
|
||||
use crate::domain::acme::store;
|
||||
use crate::error::unexpected::Mappable;
|
||||
|
||||
use std::sync;
|
||||
|
||||
struct CertResolver(sync::Arc<dyn store::Store>);
|
||||
|
||||
pub fn new(
|
||||
store: sync::Arc<dyn store::Store>,
|
||||
) -> sync::Arc<dyn rustls::server::ResolvesServerCert> {
|
||||
return sync::Arc::new(CertResolver(store));
|
||||
}
|
||||
|
||||
impl rustls::server::ResolvesServerCert for CertResolver {
|
||||
fn resolve(
|
||||
&self,
|
||||
client_hello: rustls::server::ClientHello<'_>,
|
||||
) -> Option<sync::Arc<rustls::sign::CertifiedKey>> {
|
||||
let domain = client_hello.server_name()?;
|
||||
|
||||
match self.0.get_certificate(domain) {
|
||||
Err(store::GetCertificateError::NotFound) => {
|
||||
log::warn!("No cert found for domain {domain}");
|
||||
Ok(None)
|
||||
}
|
||||
Err(store::GetCertificateError::Unexpected(err)) => Err(err),
|
||||
Ok((key, cert)) => {
|
||||
match rustls::sign::any_supported_type(&key.into()).or_unexpected() {
|
||||
Err(err) => Err(err),
|
||||
Ok(key) => Ok(Some(sync::Arc::new(rustls::sign::CertifiedKey {
|
||||
cert: cert.into_iter().map(|cert| cert.into()).collect(),
|
||||
key,
|
||||
ocsp: None,
|
||||
sct_list: None,
|
||||
}))),
|
||||
}
|
||||
}
|
||||
}
|
||||
.unwrap_or_else(|err| {
|
||||
log::error!("Unexpected error getting cert for domain {domain}: {err}");
|
||||
None
|
||||
})
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
use std::io::{Read, Write};
|
||||
use std::str::FromStr;
|
||||
use std::{fs, path};
|
||||
use std::{fs, path, sync};
|
||||
|
||||
use crate::domain::acme::{Certificate, PrivateKey};
|
||||
use crate::error::unexpected::{self, Mappable};
|
||||
@ -70,7 +70,7 @@ struct FSStore {
|
||||
dir_path: path::PathBuf,
|
||||
}
|
||||
|
||||
pub fn new(dir_path: &path::Path) -> Result<Box<dyn Store>, unexpected::Error> {
|
||||
pub fn new(dir_path: &path::Path) -> Result<sync::Arc<dyn Store>, unexpected::Error> {
|
||||
vec![
|
||||
dir_path,
|
||||
dir_path.join("http01_challenge_keys").as_ref(),
|
||||
@ -82,7 +82,7 @@ pub fn new(dir_path: &path::Path) -> Result<Box<dyn Store>, unexpected::Error> {
|
||||
})
|
||||
.try_collect()?;
|
||||
|
||||
Ok(Box::new(FSStore {
|
||||
Ok(sync::Arc::new(FSStore {
|
||||
dir_path: dir_path.into(),
|
||||
}))
|
||||
}
|
||||
|
@ -90,7 +90,7 @@ impl DNSChecker {
|
||||
|
||||
// check that the TXT record with the challenge token is correctly installed on the domain
|
||||
{
|
||||
let domain = Name::from_str("_domani_challenge")
|
||||
let domain = Name::from_str("_domiply_challenge")
|
||||
.or_unexpected_while("parsing TXT name")?
|
||||
.append_domain(domain)
|
||||
.or_unexpected_while("appending domain to TXT")?;
|
||||
|
@ -1,6 +1,6 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::str::FromStr;
|
||||
use std::{fs, io};
|
||||
use std::{fs, io, sync};
|
||||
|
||||
use crate::error::unexpected::{self, Intoable, Mappable};
|
||||
use crate::{domain, origin};
|
||||
@ -49,9 +49,9 @@ struct FSStore {
|
||||
dir_path: PathBuf,
|
||||
}
|
||||
|
||||
pub fn new(dir_path: &Path) -> io::Result<Box<dyn Store>> {
|
||||
pub fn new(dir_path: &Path) -> io::Result<sync::Arc<dyn Store>> {
|
||||
fs::create_dir_all(dir_path)?;
|
||||
Ok(Box::new(FSStore {
|
||||
Ok(sync::Arc::new(FSStore {
|
||||
dir_path: dir_path.into(),
|
||||
}))
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
use crate::domain::{self, acme, checker, config};
|
||||
use crate::error::unexpected::{self, Mappable};
|
||||
use crate::origin;
|
||||
use crate::util;
|
||||
|
||||
use std::{future, pin, sync};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
@ -117,8 +116,8 @@ impl From<config::SetError> for SyncWithConfigError {
|
||||
|
||||
pub type GetAcmeHttp01ChallengeKeyError = acme::manager::GetHttp01ChallengeKeyError;
|
||||
|
||||
//#[mockall::automock]
|
||||
pub trait Manager: Sync + Send + rustls::server::ResolvesServerCert {
|
||||
#[mockall::automock]
|
||||
pub trait Manager: Sync + Send {
|
||||
fn get_config(&self, domain: &domain::Name) -> Result<config::Config, GetConfigError>;
|
||||
|
||||
fn get_origin(
|
||||
@ -145,57 +144,70 @@ pub trait Manager: Sync + Send + rustls::server::ResolvesServerCert {
|
||||
fn all_domains(&self) -> Result<Vec<domain::Name>, unexpected::Error>;
|
||||
}
|
||||
|
||||
struct ManagerImpl {
|
||||
origin_store: Box<dyn origin::store::Store>,
|
||||
domain_config_store: Box<dyn config::Store>,
|
||||
pub struct ManagerImpl {
|
||||
origin_store: sync::Arc<dyn origin::store::Store>,
|
||||
domain_config_store: sync::Arc<dyn config::Store>,
|
||||
domain_checker: checker::DNSChecker,
|
||||
acme_manager: Option<Box<dyn acme::manager::Manager>>,
|
||||
acme_manager: Option<sync::Arc<dyn acme::manager::Manager>>,
|
||||
|
||||
canceller: CancellationToken,
|
||||
origin_sync_handler: tokio::task::JoinHandle<()>,
|
||||
}
|
||||
|
||||
async fn sync_origins(origin_store: &dyn origin::store::Store, canceller: CancellationToken) {
|
||||
let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(20 * 60));
|
||||
loop {
|
||||
tokio::select! {
|
||||
_ = interval.tick() => {
|
||||
match origin_store.all_descrs() {
|
||||
Ok(iter) => iter.into_iter(),
|
||||
Err(err) => {
|
||||
log::error!("Error fetching origin descriptors: {err}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
.for_each(|descr| {
|
||||
if let Err(err) = origin_store.sync(descr.clone(), origin::store::Limits {}) {
|
||||
log::error!("Failed to sync store for {:?}: {err}", descr);
|
||||
return;
|
||||
}
|
||||
});
|
||||
},
|
||||
_ = canceller.cancelled() => return,
|
||||
fn sync_origins(origin_store: &dyn origin::store::Store) {
|
||||
match origin_store.all_descrs() {
|
||||
Ok(iter) => iter.into_iter(),
|
||||
Err(err) => {
|
||||
log::error!("Error fetching origin descriptors: {err}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
.for_each(|descr| {
|
||||
if let Err(err) = origin_store.sync(descr.clone(), origin::store::Limits {}) {
|
||||
log::error!("Failed to sync store for {:?}: {err}", descr);
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn new(
|
||||
task_stack: &mut util::TaskStack<unexpected::Error>,
|
||||
origin_store: Box<dyn origin::store::Store>,
|
||||
domain_config_store: Box<dyn config::Store>,
|
||||
origin_store: sync::Arc<dyn origin::store::Store>,
|
||||
domain_config_store: sync::Arc<dyn config::Store>,
|
||||
domain_checker: checker::DNSChecker,
|
||||
acme_manager: Option<Box<dyn acme::manager::Manager>>,
|
||||
) -> sync::Arc<dyn Manager> {
|
||||
let manager = sync::Arc::new(ManagerImpl {
|
||||
acme_manager: Option<sync::Arc<dyn acme::manager::Manager>>,
|
||||
) -> ManagerImpl {
|
||||
let canceller = CancellationToken::new();
|
||||
|
||||
let origin_sync_handler = {
|
||||
let origin_store = origin_store.clone();
|
||||
let canceller = canceller.clone();
|
||||
tokio::spawn(async move {
|
||||
let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(20 * 60));
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
_ = interval.tick() => sync_origins(origin_store.as_ref()),
|
||||
_ = canceller.cancelled() => return,
|
||||
}
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
ManagerImpl {
|
||||
origin_store,
|
||||
domain_config_store,
|
||||
domain_checker,
|
||||
acme_manager,
|
||||
});
|
||||
canceller,
|
||||
origin_sync_handler,
|
||||
}
|
||||
}
|
||||
|
||||
task_stack.push_spawn(|canceller| {
|
||||
let manager = manager.clone();
|
||||
async move { Ok(sync_origins(manager.origin_store.as_ref(), canceller).await) }
|
||||
});
|
||||
|
||||
manager
|
||||
impl ManagerImpl {
|
||||
pub fn stop(self) -> tokio::task::JoinHandle<()> {
|
||||
self.canceller.cancel();
|
||||
self.origin_sync_handler
|
||||
}
|
||||
}
|
||||
|
||||
impl Manager for ManagerImpl {
|
||||
@ -271,35 +283,3 @@ impl Manager for ManagerImpl {
|
||||
self.domain_config_store.all_domains()
|
||||
}
|
||||
}
|
||||
|
||||
impl rustls::server::ResolvesServerCert for ManagerImpl {
|
||||
fn resolve(
|
||||
&self,
|
||||
client_hello: rustls::server::ClientHello<'_>,
|
||||
) -> Option<sync::Arc<rustls::sign::CertifiedKey>> {
|
||||
let domain = client_hello.server_name()?;
|
||||
|
||||
match self.acme_manager.as_ref()?.get_certificate(domain) {
|
||||
Err(acme::manager::GetCertificateError::NotFound) => {
|
||||
log::warn!("No cert found for domain {domain}");
|
||||
Ok(None)
|
||||
}
|
||||
Err(acme::manager::GetCertificateError::Unexpected(err)) => Err(err),
|
||||
Ok((key, cert)) => {
|
||||
match rustls::sign::any_supported_type(&key.into()).or_unexpected() {
|
||||
Err(err) => Err(err),
|
||||
Ok(key) => Ok(Some(sync::Arc::new(rustls::sign::CertifiedKey {
|
||||
cert: cert.into_iter().map(|cert| cert.into()).collect(),
|
||||
key,
|
||||
ocsp: None,
|
||||
sct_list: None,
|
||||
}))),
|
||||
}
|
||||
}
|
||||
}
|
||||
.unwrap_or_else(|err| {
|
||||
log::error!("Unexpected error getting cert for domain {domain}: {err}");
|
||||
None
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -103,28 +103,6 @@ impl<T, E: error::Error> Mappable<T> for Result<T, E> {
|
||||
}
|
||||
}
|
||||
|
||||
static OPTION_NONE_ERROR: &'static str = "expected Some but got None";
|
||||
|
||||
impl<T> Mappable<T> for Option<T> {
|
||||
fn or_unexpected(self) -> Result<T, Error> {
|
||||
self.ok_or(Error::from(OPTION_NONE_ERROR)).or_unexpected()
|
||||
}
|
||||
|
||||
fn or_unexpected_while<D: fmt::Display>(self, prefix: D) -> Result<T, Error> {
|
||||
self.ok_or(Error::from(OPTION_NONE_ERROR))
|
||||
.or_unexpected_while(prefix)
|
||||
}
|
||||
|
||||
fn map_unexpected_while<F, D>(self, f: F) -> Result<T, Error>
|
||||
where
|
||||
F: FnOnce() -> D,
|
||||
D: fmt::Display,
|
||||
{
|
||||
self.ok_or(Error::from(OPTION_NONE_ERROR))
|
||||
.map_unexpected_while(f)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Intoable {
|
||||
fn into_unexpected(self) -> Error;
|
||||
|
||||
|
136
src/main.rs
136
src/main.rs
@ -1,65 +1,70 @@
|
||||
#![feature(trait_upcasting)]
|
||||
|
||||
use clap::Parser;
|
||||
use futures::stream::StreamExt;
|
||||
use signal_hook_tokio::Signals;
|
||||
|
||||
use std::net::SocketAddr;
|
||||
use std::path;
|
||||
use std::str::FromStr;
|
||||
use std::{path, sync};
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(version)]
|
||||
#[command(about = "A domani to another dimension")]
|
||||
#[command(about = "A domiply to another dimension")]
|
||||
struct Cli {
|
||||
#[arg(
|
||||
long,
|
||||
help = "OFF, ERROR, WARN, INFO, DEBUG, or TRACE",
|
||||
default_value_t = log::LevelFilter::Info,
|
||||
env = "DOMANI_LOG_LEVEL"
|
||||
env = "DOMIPLY_LOG_LEVEL"
|
||||
)]
|
||||
log_level: log::LevelFilter,
|
||||
|
||||
#[arg(long, default_value_t = false, env = "DOMANI_LOG_TIMESTAMP")]
|
||||
#[arg(long, default_value_t = false, env = "DOMIPLY_LOG_TIMESTAMP")]
|
||||
log_timestamp: bool,
|
||||
|
||||
#[arg(long, required = true, env = "DOMANI_HTTP_DOMAIN")]
|
||||
http_domain: domani::domain::Name,
|
||||
#[arg(long, required = true, env = "DOMIPLY_HTTP_DOMAIN")]
|
||||
http_domain: domiply::domain::Name,
|
||||
|
||||
#[arg(long, default_value_t = SocketAddr::from_str("[::]:3030").unwrap(), env = "DOMANI_HTTP_LISTEN_ADDR")]
|
||||
#[arg(long, default_value_t = SocketAddr::from_str("[::]:3030").unwrap(), env = "DOMIPLY_HTTP_LISTEN_ADDR")]
|
||||
http_listen_addr: SocketAddr,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
help = "E.g. '[::]:443', if given then SSL certs will automatically be retrieved for all domains using LetsEncrypt",
|
||||
env = "DOMANI_HTTPS_LISTEN_ADDR",
|
||||
env = "DOMIPLY_HTTPS_LISTEN_ADDR",
|
||||
requires = "domain_acme_contact_email",
|
||||
requires = "domain_acme_store_dir_path"
|
||||
)]
|
||||
https_listen_addr: Option<SocketAddr>,
|
||||
|
||||
#[arg(long, required = true, env = "DOMANI_PASSPHRASE")]
|
||||
#[arg(long, required = true, env = "DOMIPLY_PASSPHRASE")]
|
||||
passphrase: String,
|
||||
|
||||
#[arg(long, required = true, env = "DOMANI_ORIGIN_STORE_GIT_DIR_PATH")]
|
||||
#[arg(long, required = true, env = "DOMIPLY_ORIGIN_STORE_GIT_DIR_PATH")]
|
||||
origin_store_git_dir_path: path::PathBuf,
|
||||
|
||||
#[arg(long, required = true, env = "DOMANI_DOMAIN_CHECKER_TARGET_A")]
|
||||
#[arg(long, required = true, env = "DOMIPLY_DOMAIN_CHECKER_TARGET_A")]
|
||||
domain_checker_target_a: std::net::Ipv4Addr,
|
||||
|
||||
#[arg(long, default_value_t = String::from("1.1.1.1:53"), env = "DOMANI_DOMAIN_CHECKER_RESOLVER_ADDR")]
|
||||
#[arg(long, default_value_t = String::from("1.1.1.1:53"), env = "DOMIPLY_DOMAIN_CHECKER_RESOLVER_ADDR")]
|
||||
domain_checker_resolver_addr: String,
|
||||
|
||||
#[arg(long, required = true, env = "DOMANI_DOMAIN_CONFIG_STORE_DIR_PATH")]
|
||||
#[arg(long, required = true, env = "DOMIPLY_DOMAIN_CONFIG_STORE_DIR_PATH")]
|
||||
domain_config_store_dir_path: path::PathBuf,
|
||||
|
||||
#[arg(long, env = "DOMANI_DOMAIN_ACME_STORE_DIR_PATH")]
|
||||
#[arg(long, env = "DOMIPLY_DOMAIN_ACME_STORE_DIR_PATH")]
|
||||
domain_acme_store_dir_path: Option<path::PathBuf>,
|
||||
|
||||
#[arg(long, env = "DOMANI_DOMAIN_ACME_CONTACT_EMAIL")]
|
||||
#[arg(long, env = "DOMIPLY_DOMAIN_ACME_CONTACT_EMAIL")]
|
||||
domain_acme_contact_email: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct HTTPSParams {
|
||||
https_listen_addr: SocketAddr,
|
||||
domain_acme_store: sync::Arc<dyn domiply::domain::acme::store::Store>,
|
||||
domain_acme_manager: sync::Arc<dyn domiply::domain::acme::manager::Manager>,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let config = Cli::parse();
|
||||
@ -73,81 +78,98 @@ async fn main() {
|
||||
)
|
||||
.init();
|
||||
|
||||
let origin_store = domani::origin::store::git::new(config.origin_store_git_dir_path)
|
||||
let canceller = tokio_util::sync::CancellationToken::new();
|
||||
|
||||
{
|
||||
let canceller = canceller.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
let mut signals = Signals::new(signal_hook::consts::TERM_SIGNALS)
|
||||
.expect("initializing signals failed");
|
||||
|
||||
if (signals.next().await).is_some() {
|
||||
log::info!("Gracefully shutting down...");
|
||||
canceller.cancel();
|
||||
}
|
||||
|
||||
if (signals.next().await).is_some() {
|
||||
log::warn!("Forcefully shutting down");
|
||||
std::process::exit(1);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
let origin_store = domiply::origin::store::git::new(config.origin_store_git_dir_path)
|
||||
.expect("git origin store initialization failed");
|
||||
|
||||
let domain_checker = domani::domain::checker::new(
|
||||
let domain_checker = domiply::domain::checker::new(
|
||||
config.domain_checker_target_a,
|
||||
&config.domain_checker_resolver_addr,
|
||||
)
|
||||
.await
|
||||
.expect("domain checker initialization failed");
|
||||
|
||||
let domain_config_store = domani::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 initialization failed");
|
||||
|
||||
let 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_dir_path = config.domain_acme_store_dir_path.unwrap();
|
||||
|
||||
let domain_acme_store = domani::domain::acme::store::new(&domain_acme_store_dir_path)
|
||||
let domain_acme_store = domiply::domain::acme::store::new(&domain_acme_store_dir_path)
|
||||
.expect("domain acme store initialization failed");
|
||||
|
||||
// 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();
|
||||
|
||||
Some(
|
||||
domani::domain::acme::manager::new(domain_acme_store, &domain_acme_contact_email)
|
||||
.await
|
||||
.expect("domain acme manager initialization failed"),
|
||||
let domain_acme_manager = domiply::domain::acme::manager::new(
|
||||
domain_acme_store.clone(),
|
||||
&domain_acme_contact_email,
|
||||
)
|
||||
.await
|
||||
.expect("domain acme manager initialization failed");
|
||||
|
||||
Some(HTTPSParams {
|
||||
https_listen_addr,
|
||||
domain_acme_store,
|
||||
domain_acme_manager,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let mut task_stack = domani::util::TaskStack::new();
|
||||
|
||||
let domain_manager = domani::domain::manager::new(
|
||||
&mut task_stack,
|
||||
let domain_manager = domiply::domain::manager::new(
|
||||
origin_store,
|
||||
domain_config_store,
|
||||
domain_checker,
|
||||
domain_acme_manager,
|
||||
https_params.as_ref().map(|p| p.domain_acme_manager.clone()),
|
||||
);
|
||||
|
||||
let _ = domani::service::http::new(
|
||||
&mut task_stack,
|
||||
domain_manager.clone(),
|
||||
config.domain_checker_target_a,
|
||||
config.passphrase,
|
||||
config.http_listen_addr.clone(),
|
||||
config.http_domain.clone(),
|
||||
config
|
||||
.https_listen_addr
|
||||
.map(|listen_addr| domani::service::http::HTTPSParams {
|
||||
listen_addr,
|
||||
cert_resolver: domain_manager.clone(),
|
||||
let domain_manager = sync::Arc::new(domain_manager);
|
||||
|
||||
{
|
||||
let (http_service, http_service_task_set) = domiply::service::http::new(
|
||||
domain_manager.clone(),
|
||||
config.domain_checker_target_a,
|
||||
config.passphrase,
|
||||
config.http_listen_addr.clone(),
|
||||
config.http_domain.clone(),
|
||||
https_params.map(|p| domiply::service::http::HTTPSParams {
|
||||
listen_addr: p.https_listen_addr,
|
||||
cert_resolver: domiply::domain::acme::resolver::new(p.domain_acme_store),
|
||||
}),
|
||||
);
|
||||
);
|
||||
|
||||
let mut signals =
|
||||
Signals::new(signal_hook::consts::TERM_SIGNALS).expect("initializing signals failed");
|
||||
canceller.cancelled().await;
|
||||
|
||||
if (signals.next().await).is_some() {
|
||||
log::info!("Gracefully shutting down...");
|
||||
domiply::service::http::stop(http_service, http_service_task_set).await;
|
||||
}
|
||||
|
||||
tokio::spawn(async move {
|
||||
if (signals.next().await).is_some() {
|
||||
log::warn!("Forcefully shutting down");
|
||||
std::process::exit(1);
|
||||
};
|
||||
});
|
||||
|
||||
task_stack
|
||||
sync::Arc::into_inner(domain_manager)
|
||||
.unwrap()
|
||||
.stop()
|
||||
.await
|
||||
.expect("failed to stop all background tasks");
|
||||
.expect("domain manager failed to shutdown cleanly");
|
||||
|
||||
log::info!("Graceful shutdown complete");
|
||||
}
|
||||
|
@ -71,9 +71,9 @@ struct Store {
|
||||
origins: sync::RwLock<collections::HashMap<origin::Descr, sync::Arc<Origin>>>,
|
||||
}
|
||||
|
||||
pub fn new(dir_path: PathBuf) -> io::Result<Box<dyn super::Store>> {
|
||||
pub fn new(dir_path: PathBuf) -> io::Result<sync::Arc<dyn super::Store>> {
|
||||
fs::create_dir_all(&dir_path)?;
|
||||
Ok(Box::new(Store {
|
||||
Ok(sync::Arc::new(Store {
|
||||
dir_path,
|
||||
sync_guard: sync::Mutex::new(collections::HashMap::new()),
|
||||
origins: sync::RwLock::new(collections::HashMap::new()),
|
||||
|
@ -27,14 +27,13 @@ pub struct HTTPSParams {
|
||||
}
|
||||
|
||||
pub fn new(
|
||||
task_stack: &mut util::TaskStack<unexpected::Error>,
|
||||
domain_manager: sync::Arc<dyn domain::manager::Manager>,
|
||||
target_a: net::Ipv4Addr,
|
||||
passphrase: String,
|
||||
http_listen_addr: net::SocketAddr,
|
||||
http_domain: domain::Name,
|
||||
https_params: Option<HTTPSParams>,
|
||||
) -> sync::Arc<Service> {
|
||||
) -> (sync::Arc<Service>, util::TaskSet<unexpected::Error>) {
|
||||
let service = sync::Arc::new(Service {
|
||||
domain_manager: domain_manager.clone(),
|
||||
target_a,
|
||||
@ -43,7 +42,9 @@ pub fn new(
|
||||
handlebars: tpl::get(),
|
||||
});
|
||||
|
||||
task_stack.push_spawn(|canceller| {
|
||||
let task_set = util::TaskSet::new();
|
||||
|
||||
task_set.spawn(|canceller| {
|
||||
tasks::listen_http(
|
||||
service.clone(),
|
||||
canceller,
|
||||
@ -53,7 +54,7 @@ pub fn new(
|
||||
});
|
||||
|
||||
if let Some(https_params) = https_params {
|
||||
task_stack.push_spawn(|canceller| {
|
||||
task_set.spawn(|canceller| {
|
||||
tasks::listen_https(
|
||||
service.clone(),
|
||||
canceller,
|
||||
@ -63,12 +64,21 @@ pub fn new(
|
||||
)
|
||||
});
|
||||
|
||||
task_stack.push_spawn(|canceller| {
|
||||
task_set.spawn(|canceller| {
|
||||
tasks::cert_refresher(domain_manager.clone(), canceller, http_domain.clone())
|
||||
});
|
||||
}
|
||||
|
||||
return service;
|
||||
return (service, task_set);
|
||||
}
|
||||
|
||||
pub async fn stop(service: sync::Arc<Service>, task_set: util::TaskSet<unexpected::Error>) {
|
||||
task_set
|
||||
.stop()
|
||||
.await
|
||||
.iter()
|
||||
.for_each(|e| log::error!("error while shutting down http service: {e}"));
|
||||
sync::Arc::into_inner(service).expect("service didn't get cleaned up");
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
@ -378,7 +388,7 @@ impl<'svc> Service {
|
||||
return self.serve_origin(domain, req.uri().path());
|
||||
}
|
||||
|
||||
// Serve main domani site
|
||||
// Serve main domiply site
|
||||
|
||||
if method == Method::GET && path.starts_with("/static/") {
|
||||
return self.render(200, path, ());
|
||||
|
@ -6,7 +6,7 @@
|
||||
|
||||
<meta charset="UTF-8" />
|
||||
|
||||
<title>Domani - The universal, zero-authentication hosting service</title>
|
||||
<title>Domiply - The universal, zero-authentication hosting service</title>
|
||||
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
|
||||
@ -28,7 +28,7 @@
|
||||
<body>
|
||||
|
||||
<header>
|
||||
<h1><a href="/">Domani</a></h1>
|
||||
<h1><a href="/">Domiply</a></h1>
|
||||
<blockquote>The universal, zero-authentication hosting service</blockquote>
|
||||
</header>
|
||||
|
||||
|
@ -5,19 +5,19 @@
|
||||
{{# if data.config }}
|
||||
|
||||
<p>Your domain <code>{{ data.domain }}</code> is already configured with
|
||||
Domani. You can see the existing configuration below. If you modify any values
|
||||
Domiply. You can see the existing configuration below. If you modify any values
|
||||
you will need to hit the "Next" button to complete the update.</p>
|
||||
|
||||
{{ else }}
|
||||
|
||||
<p>Your domain <code>{{ data.domain }}</code> is not yet configured with Domani.
|
||||
<p>Your domain <code>{{ data.domain }}</code> is not yet configured with Domiply.
|
||||
To get started, please input the details of a public git repo which will be used
|
||||
to serve your domain. When you update the given branch, your domain will be
|
||||
automatically updated too!</p>
|
||||
|
||||
{{/if}}
|
||||
|
||||
<p><em>In the future Domani will support more backends than just git
|
||||
<p><em>In the future Domiply will support more backends than just git
|
||||
repos.</em></p>
|
||||
|
||||
<form method="GET" action="/domain_init.html">
|
||||
|
@ -1,6 +1,6 @@
|
||||
<h2>Configure DNS</h2>
|
||||
|
||||
<p>Next you will need to configure your DNS server to point to Domani. There
|
||||
<p>Next you will need to configure your DNS server to point to Domiply. There
|
||||
are two entries you will need to add:</p>
|
||||
|
||||
<ul>
|
||||
@ -9,7 +9,7 @@ are two entries you will need to add:</p>
|
||||
<code>{{ data.target_a }}</code>
|
||||
</li>
|
||||
<li>
|
||||
A <code>TXT _domani_challenge.{{ data.domain }}</code> entry with the value
|
||||
A <code>TXT _domiply_challenge.{{ data.domain }}</code> entry with the value
|
||||
<code>{{ data.challenge_token }}</code>
|
||||
</li>
|
||||
</ul>
|
||||
|
@ -1,6 +1,6 @@
|
||||
<h2>All Domains</h2>
|
||||
|
||||
<p>Below are listed all domains which this Domani instance is currently
|
||||
<p>Below are listed all domains which this Domiply instance is currently
|
||||
serving</p>
|
||||
|
||||
<ul>
|
||||
|
@ -1,11 +1,11 @@
|
||||
<p>Domani connects your domain to whatever you want to host on it, all with no
|
||||
<p>Domiply connects your domain to whatever you want to host on it, all with no
|
||||
account needed. Just input your desired backend, add two entries to your DNS
|
||||
server, and you're done!</p>
|
||||
|
||||
<p><strong>YOU SHOULD NOT USE THIS FOR ANYTHING YOU CARE ABOUT AT THIS
|
||||
TIME.</strong></p>
|
||||
|
||||
<p>Domani is currently only a proof-of-concept with limited features,
|
||||
<p>Domiply is currently only a proof-of-concept with limited features,
|
||||
but will continue to be expanded as development time permits.</p>
|
||||
|
||||
<h2>Get Started</h2>
|
||||
@ -30,20 +30,20 @@ been set up.</p>
|
||||
|
||||
<ul>
|
||||
<li><a href="/domains.html">List all existing domains</a></li>
|
||||
<li><a href="https://code.betamike.com/cryptic-io/domani">View the Source Code</a></li>
|
||||
<li><a href="https://code.betamike.com/cryptic-io/domiply">View the Source Code</a></li>
|
||||
<li><a href="mailto:me@mediocregopher.com">Report a Bug</a></li>
|
||||
</ul>
|
||||
|
||||
<h2>About</h2>
|
||||
|
||||
<p>Domani is an open-source project which is designed to be hosted by
|
||||
<p>Domiply is an open-source project which is designed to be hosted by
|
||||
individuals for their community of friends and family. By making it super easy
|
||||
to set up a domain we can help our non-technical folk own their own slice of
|
||||
the internet, the way it was always intended.</p>
|
||||
|
||||
<h2>Roadmap</h2>
|
||||
|
||||
<p>Domani is very much a work in progress. The following functionality is
|
||||
<p>Domiply is very much a work in progress. The following functionality is
|
||||
planned but not yet implemented:</p>
|
||||
|
||||
<ul>
|
||||
|
57
src/util.rs
57
src/util.rs
@ -1,5 +1,6 @@
|
||||
use std::{error, fs, io, path, pin};
|
||||
use std::{error, fs, io, path};
|
||||
|
||||
use futures::stream::futures_unordered::FuturesUnordered;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
pub fn open_file(path: &path::Path) -> io::Result<Option<fs::File>> {
|
||||
@ -12,60 +13,44 @@ pub fn open_file(path: &path::Path) -> io::Result<Option<fs::File>> {
|
||||
}
|
||||
}
|
||||
|
||||
type StaticFuture<O> = pin::Pin<Box<dyn futures::Future<Output = O> + Send + 'static>>;
|
||||
|
||||
pub struct TaskStack<E>
|
||||
pub struct TaskSet<E>
|
||||
where
|
||||
E: error::Error + Send + 'static,
|
||||
{
|
||||
wait_group: Vec<StaticFuture<Result<(), E>>>,
|
||||
canceller: CancellationToken,
|
||||
wait_group: FuturesUnordered<tokio::task::JoinHandle<Result<(), E>>>,
|
||||
}
|
||||
|
||||
impl<E> TaskStack<E>
|
||||
impl<E> TaskSet<E>
|
||||
where
|
||||
E: error::Error + Send + 'static,
|
||||
{
|
||||
pub fn new() -> TaskStack<E> {
|
||||
TaskStack {
|
||||
wait_group: Vec::new(),
|
||||
pub fn new() -> TaskSet<E> {
|
||||
TaskSet {
|
||||
canceller: CancellationToken::new(),
|
||||
wait_group: FuturesUnordered::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// push adds the given Future to the stack, to be executed once stop is called.
|
||||
pub fn push<Fut>(&mut self, f: Fut)
|
||||
where
|
||||
Fut: futures::Future<Output = Result<(), E>> + Send + 'static,
|
||||
{
|
||||
self.wait_group.push(Box::pin(f))
|
||||
}
|
||||
|
||||
/// push_spawn will spawn the given closure in a tokio task. Once the CancellationToken is
|
||||
/// cancelled the closure is expected to return.
|
||||
pub fn push_spawn<F, Fut>(&mut self, mut f: F)
|
||||
pub fn spawn<F, Fut>(&self, mut f: F)
|
||||
where
|
||||
Fut: futures::Future<Output = Result<(), E>> + Send + 'static,
|
||||
F: FnMut(CancellationToken) -> Fut,
|
||||
{
|
||||
let canceller = CancellationToken::new();
|
||||
let handle = tokio::spawn(f(canceller.clone()));
|
||||
self.push(async move {
|
||||
canceller.cancel();
|
||||
handle.await.expect("failed to join task")
|
||||
});
|
||||
let canceller = self.canceller.clone();
|
||||
let handle = tokio::spawn(f(canceller));
|
||||
self.wait_group.push(handle);
|
||||
}
|
||||
|
||||
/// stop will process all operations which have been pushed onto the stack in the reverse order
|
||||
/// they were pushed.
|
||||
pub async fn stop(mut self) -> Result<(), E> {
|
||||
// reverse wait_group in place, so we stop the most recently added first. Since this method
|
||||
// consumes self this is fine.
|
||||
self.wait_group.reverse();
|
||||
pub async fn stop(self) -> Vec<E> {
|
||||
self.canceller.cancel();
|
||||
|
||||
for fut in self.wait_group {
|
||||
if let Err(err) = fut.await {
|
||||
return Err(err);
|
||||
let mut res = Vec::new();
|
||||
for f in self.wait_group {
|
||||
if let Err(err) = f.await.expect("task failed") {
|
||||
res.push(err);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
res
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user