Compare commits

..

No commits in common. "644d2bab23381d5bcdb8c48480dce8f12ccda65e" and "f2374cded5e3a67d181c309522cc3649672af524" have entirely different histories.

22 changed files with 269 additions and 263 deletions

View File

@ -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
View File

@ -444,7 +444,7 @@ dependencies = [
]
[[package]]
name = "domani"
name = "domiply"
version = "0.1.0"
dependencies = [
"acme2",

View File

@ -1,5 +1,5 @@
[package]
name = "domani"
name = "domiply"
version = "0.1.0"
edition = "2021"

View File

@ -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
View File

@ -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

View File

@ -1,4 +1,5 @@
pub mod manager;
pub mod resolver;
pub mod store;
mod private_key;

View File

@ -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)
}
}

View 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
})
}
}

View File

@ -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(),
}))
}

View File

@ -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")?;

View File

@ -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(),
}))
}

View File

@ -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
})
}
}

View File

@ -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;

View File

@ -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");
}

View File

@ -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()),

View File

@ -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, ());

View File

@ -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>

View File

@ -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">

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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
}
}