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 DOMIPLY_HTTP_DOMAIN=localhost
|
||||||
export DOMANI_PASSPHRASE=foobar
|
export DOMIPLY_PASSPHRASE=foobar
|
||||||
export DOMANI_ORIGIN_STORE_GIT_DIR_PATH=/tmp/domani_dev_env/origin/git
|
export DOMIPLY_ORIGIN_STORE_GIT_DIR_PATH=/tmp/domiply_dev_env/origin/git
|
||||||
export DOMANI_DOMAIN_CHECKER_TARGET_A=127.0.0.1
|
export DOMIPLY_DOMAIN_CHECKER_TARGET_A=127.0.0.1
|
||||||
export DOMANI_DOMAIN_CONFIG_STORE_DIR_PATH=/tmp/domani_dev_env/domain/config
|
export DOMIPLY_DOMAIN_CONFIG_STORE_DIR_PATH=/tmp/domiply_dev_env/domain/config
|
||||||
export DOMANI_DOMAIN_ACME_STORE_DIR_PATH=/tmp/domani_dev_env/domain/acme
|
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]]
|
[[package]]
|
||||||
name = "domani"
|
name = "domiply"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"acme2",
|
"acme2",
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "domani"
|
name = "domiply"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
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
|
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
|
inputs their domain name, their desired backend, and then adds two entries to
|
||||||
their DNS server.
|
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
|
## 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:
|
In order to create a release binary:
|
||||||
|
|
||||||
@ -21,38 +21,38 @@ A statically compiled binary will be placed in the `result` directory.
|
|||||||
|
|
||||||
## Configuration
|
## 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>
|
--http-domain <HTTP_DOMAIN>
|
||||||
[env: DOMANI_HTTP_DOMAIN=]
|
[env: DOMIPLY_HTTP_DOMAIN=]
|
||||||
|
|
||||||
--http-listen-addr <HTTP_LISTEN_ADDR>
|
--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>
|
--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>
|
--passphrase <PASSPHRASE>
|
||||||
[env: DOMANI_PASSPHRASE=]
|
[env: DOMIPLY_PASSPHRASE=]
|
||||||
|
|
||||||
--origin-store-git-dir-path <ORIGIN_STORE_GIT_DIR_PATH>
|
--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>
|
--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>
|
--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>
|
--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>
|
--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>
|
--domain-acme-contact-email <DOMAIN_ACME_CONTACT_EMAIL>
|
||||||
[env: DOMANI_DOMAIN_ACME_CONTACT_EMAIL=]
|
[env: DOMIPLY_DOMAIN_ACME_CONTACT_EMAIL=]
|
||||||
|
|
||||||
-h, --help
|
-h, --help
|
||||||
Print help
|
Print help
|
||||||
@ -63,8 +63,8 @@ Domani is configured via command-line arguments or environment variables:
|
|||||||
|
|
||||||
### HTTPS Support
|
### HTTPS Support
|
||||||
|
|
||||||
Domani will automatically handle setting up HTTPS via LetsEncrypt for both the
|
Domiply 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 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
|
By default HTTPS is not enabled, but can be easily enabled by setting the
|
||||||
following arguments:
|
following arguments:
|
||||||
@ -81,7 +81,7 @@ secured as best as possible.
|
|||||||
|
|
||||||
## Development
|
## 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
|
In order to open a shell with all necessary tooling (expected rust toolchain
|
||||||
versions, etc...) simply do:
|
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 manager;
|
||||||
|
pub mod resolver;
|
||||||
pub mod store;
|
pub mod store;
|
||||||
|
|
||||||
mod private_key;
|
mod private_key;
|
||||||
|
@ -1,13 +1,11 @@
|
|||||||
use std::{future, pin, sync, time};
|
use std::{future, pin, sync, time};
|
||||||
|
|
||||||
use crate::domain::acme::{Certificate, PrivateKey};
|
|
||||||
use crate::domain::{self, acme};
|
use crate::domain::{self, acme};
|
||||||
use crate::error::unexpected::{self, Intoable, Mappable};
|
use crate::error::unexpected::{self, Intoable, Mappable};
|
||||||
|
|
||||||
const LETS_ENCRYPT_URL: &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;
|
||||||
pub type GetCertificateError = acme::store::GetCertificateError;
|
|
||||||
|
|
||||||
#[mockall::automock]
|
#[mockall::automock]
|
||||||
pub trait Manager: Sync + Send {
|
pub trait Manager: Sync + Send {
|
||||||
@ -16,23 +14,17 @@ pub trait Manager: Sync + Send {
|
|||||||
domain: domain::Name,
|
domain: domain::Name,
|
||||||
) -> pin::Pin<Box<dyn future::Future<Output = Result<(), unexpected::Error>> + Send + 'mgr>>;
|
) -> pin::Pin<Box<dyn future::Future<Output = Result<(), unexpected::Error>> + Send + 'mgr>>;
|
||||||
fn get_http01_challenge_key(&self, token: &str) -> Result<String, GetHttp01ChallengeKeyError>;
|
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 {
|
struct ManagerImpl {
|
||||||
store: Box<dyn acme::store::Store>,
|
store: sync::Arc<dyn acme::store::Store>,
|
||||||
account: sync::Arc<acme2::Account>,
|
account: sync::Arc<acme2::Account>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn new(
|
pub async fn new(
|
||||||
store: Box<dyn acme::store::Store>,
|
store: sync::Arc<dyn acme::store::Store>,
|
||||||
contact_email: &str,
|
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())
|
let dir = acme2::DirectoryBuilder::new(LETS_ENCRYPT_URL.to_string())
|
||||||
.build()
|
.build()
|
||||||
.await
|
.await
|
||||||
@ -61,7 +53,6 @@ pub async fn new(
|
|||||||
.build()
|
.build()
|
||||||
.await
|
.await
|
||||||
.or_unexpected_while("building account")?;
|
.or_unexpected_while("building account")?;
|
||||||
|
|
||||||
let account_key: acme::PrivateKey = account
|
let account_key: acme::PrivateKey = account
|
||||||
.private_key()
|
.private_key()
|
||||||
.as_ref()
|
.as_ref()
|
||||||
@ -72,7 +63,7 @@ pub async fn new(
|
|||||||
.set_account_key(&account_key)
|
.set_account_key(&account_key)
|
||||||
.or_unexpected_while("storing 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 {
|
impl Manager for ManagerImpl {
|
||||||
@ -291,12 +282,4 @@ impl Manager for ManagerImpl {
|
|||||||
fn get_http01_challenge_key(&self, token: &str) -> Result<String, GetHttp01ChallengeKeyError> {
|
fn get_http01_challenge_key(&self, token: &str) -> Result<String, GetHttp01ChallengeKeyError> {
|
||||||
self.store.get_http01_challenge_key(token)
|
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::io::{Read, Write};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::{fs, path};
|
use std::{fs, path, sync};
|
||||||
|
|
||||||
use crate::domain::acme::{Certificate, PrivateKey};
|
use crate::domain::acme::{Certificate, PrivateKey};
|
||||||
use crate::error::unexpected::{self, Mappable};
|
use crate::error::unexpected::{self, Mappable};
|
||||||
@ -70,7 +70,7 @@ struct FSStore {
|
|||||||
dir_path: path::PathBuf,
|
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![
|
vec![
|
||||||
dir_path,
|
dir_path,
|
||||||
dir_path.join("http01_challenge_keys").as_ref(),
|
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()?;
|
.try_collect()?;
|
||||||
|
|
||||||
Ok(Box::new(FSStore {
|
Ok(sync::Arc::new(FSStore {
|
||||||
dir_path: dir_path.into(),
|
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
|
// 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")?
|
.or_unexpected_while("parsing TXT name")?
|
||||||
.append_domain(domain)
|
.append_domain(domain)
|
||||||
.or_unexpected_while("appending domain to TXT")?;
|
.or_unexpected_while("appending domain to TXT")?;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::{fs, io};
|
use std::{fs, io, sync};
|
||||||
|
|
||||||
use crate::error::unexpected::{self, Intoable, Mappable};
|
use crate::error::unexpected::{self, Intoable, Mappable};
|
||||||
use crate::{domain, origin};
|
use crate::{domain, origin};
|
||||||
@ -49,9 +49,9 @@ struct FSStore {
|
|||||||
dir_path: PathBuf,
|
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)?;
|
fs::create_dir_all(dir_path)?;
|
||||||
Ok(Box::new(FSStore {
|
Ok(sync::Arc::new(FSStore {
|
||||||
dir_path: dir_path.into(),
|
dir_path: dir_path.into(),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
use crate::domain::{self, acme, checker, config};
|
use crate::domain::{self, acme, checker, config};
|
||||||
use crate::error::unexpected::{self, Mappable};
|
use crate::error::unexpected::{self, Mappable};
|
||||||
use crate::origin;
|
use crate::origin;
|
||||||
use crate::util;
|
|
||||||
|
|
||||||
use std::{future, pin, sync};
|
use std::{future, pin, sync};
|
||||||
use tokio_util::sync::CancellationToken;
|
use tokio_util::sync::CancellationToken;
|
||||||
@ -117,8 +116,8 @@ impl From<config::SetError> for SyncWithConfigError {
|
|||||||
|
|
||||||
pub type GetAcmeHttp01ChallengeKeyError = acme::manager::GetHttp01ChallengeKeyError;
|
pub type GetAcmeHttp01ChallengeKeyError = acme::manager::GetHttp01ChallengeKeyError;
|
||||||
|
|
||||||
//#[mockall::automock]
|
#[mockall::automock]
|
||||||
pub trait Manager: Sync + Send + rustls::server::ResolvesServerCert {
|
pub trait Manager: Sync + Send {
|
||||||
fn get_config(&self, domain: &domain::Name) -> Result<config::Config, GetConfigError>;
|
fn get_config(&self, domain: &domain::Name) -> Result<config::Config, GetConfigError>;
|
||||||
|
|
||||||
fn get_origin(
|
fn get_origin(
|
||||||
@ -145,18 +144,17 @@ pub trait Manager: Sync + Send + rustls::server::ResolvesServerCert {
|
|||||||
fn all_domains(&self) -> Result<Vec<domain::Name>, unexpected::Error>;
|
fn all_domains(&self) -> Result<Vec<domain::Name>, unexpected::Error>;
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ManagerImpl {
|
pub struct ManagerImpl {
|
||||||
origin_store: Box<dyn origin::store::Store>,
|
origin_store: sync::Arc<dyn origin::store::Store>,
|
||||||
domain_config_store: Box<dyn config::Store>,
|
domain_config_store: sync::Arc<dyn config::Store>,
|
||||||
domain_checker: checker::DNSChecker,
|
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) {
|
fn sync_origins(origin_store: &dyn origin::store::Store) {
|
||||||
let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(20 * 60));
|
|
||||||
loop {
|
|
||||||
tokio::select! {
|
|
||||||
_ = interval.tick() => {
|
|
||||||
match origin_store.all_descrs() {
|
match origin_store.all_descrs() {
|
||||||
Ok(iter) => iter.into_iter(),
|
Ok(iter) => iter.into_iter(),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
@ -170,32 +168,46 @@ async fn sync_origins(origin_store: &dyn origin::store::Store, canceller: Cancel
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
|
||||||
_ = canceller.cancelled() => return,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new(
|
pub fn new(
|
||||||
task_stack: &mut util::TaskStack<unexpected::Error>,
|
origin_store: sync::Arc<dyn origin::store::Store>,
|
||||||
origin_store: Box<dyn origin::store::Store>,
|
domain_config_store: sync::Arc<dyn config::Store>,
|
||||||
domain_config_store: Box<dyn config::Store>,
|
|
||||||
domain_checker: checker::DNSChecker,
|
domain_checker: checker::DNSChecker,
|
||||||
acme_manager: Option<Box<dyn acme::manager::Manager>>,
|
acme_manager: Option<sync::Arc<dyn acme::manager::Manager>>,
|
||||||
) -> sync::Arc<dyn Manager> {
|
) -> ManagerImpl {
|
||||||
let manager = sync::Arc::new(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,
|
origin_store,
|
||||||
domain_config_store,
|
domain_config_store,
|
||||||
domain_checker,
|
domain_checker,
|
||||||
acme_manager,
|
acme_manager,
|
||||||
});
|
canceller,
|
||||||
|
origin_sync_handler,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
task_stack.push_spawn(|canceller| {
|
impl ManagerImpl {
|
||||||
let manager = manager.clone();
|
pub fn stop(self) -> tokio::task::JoinHandle<()> {
|
||||||
async move { Ok(sync_origins(manager.origin_store.as_ref(), canceller).await) }
|
self.canceller.cancel();
|
||||||
});
|
self.origin_sync_handler
|
||||||
|
}
|
||||||
manager
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Manager for ManagerImpl {
|
impl Manager for ManagerImpl {
|
||||||
@ -271,35 +283,3 @@ impl Manager for ManagerImpl {
|
|||||||
self.domain_config_store.all_domains()
|
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 {
|
pub trait Intoable {
|
||||||
fn into_unexpected(self) -> Error;
|
fn into_unexpected(self) -> Error;
|
||||||
|
|
||||||
|
124
src/main.rs
124
src/main.rs
@ -1,65 +1,70 @@
|
|||||||
#![feature(trait_upcasting)]
|
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use futures::stream::StreamExt;
|
use futures::stream::StreamExt;
|
||||||
use signal_hook_tokio::Signals;
|
use signal_hook_tokio::Signals;
|
||||||
|
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
use std::path;
|
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
use std::{path, sync};
|
||||||
|
|
||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
#[command(version)]
|
#[command(version)]
|
||||||
#[command(about = "A domani to another dimension")]
|
#[command(about = "A domiply to another dimension")]
|
||||||
struct Cli {
|
struct Cli {
|
||||||
#[arg(
|
#[arg(
|
||||||
long,
|
long,
|
||||||
help = "OFF, ERROR, WARN, INFO, DEBUG, or TRACE",
|
help = "OFF, ERROR, WARN, INFO, DEBUG, or TRACE",
|
||||||
default_value_t = log::LevelFilter::Info,
|
default_value_t = log::LevelFilter::Info,
|
||||||
env = "DOMANI_LOG_LEVEL"
|
env = "DOMIPLY_LOG_LEVEL"
|
||||||
)]
|
)]
|
||||||
log_level: log::LevelFilter,
|
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,
|
log_timestamp: bool,
|
||||||
|
|
||||||
#[arg(long, required = true, env = "DOMANI_HTTP_DOMAIN")]
|
#[arg(long, required = true, env = "DOMIPLY_HTTP_DOMAIN")]
|
||||||
http_domain: domani::domain::Name,
|
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,
|
http_listen_addr: SocketAddr,
|
||||||
|
|
||||||
#[arg(
|
#[arg(
|
||||||
long,
|
long,
|
||||||
help = "E.g. '[::]:443', if given then SSL certs will automatically be retrieved for all domains using LetsEncrypt",
|
help = "E.g. '[::]:443', if given then SSL certs will automatically be retrieved for all domains using LetsEncrypt",
|
||||||
env = "DOMANI_HTTPS_LISTEN_ADDR",
|
env = "DOMIPLY_HTTPS_LISTEN_ADDR",
|
||||||
requires = "domain_acme_contact_email",
|
requires = "domain_acme_contact_email",
|
||||||
requires = "domain_acme_store_dir_path"
|
requires = "domain_acme_store_dir_path"
|
||||||
)]
|
)]
|
||||||
https_listen_addr: Option<SocketAddr>,
|
https_listen_addr: Option<SocketAddr>,
|
||||||
|
|
||||||
#[arg(long, required = true, env = "DOMANI_PASSPHRASE")]
|
#[arg(long, required = true, env = "DOMIPLY_PASSPHRASE")]
|
||||||
passphrase: String,
|
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,
|
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,
|
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,
|
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,
|
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>,
|
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>,
|
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]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
let config = Cli::parse();
|
let config = Cli::parse();
|
||||||
@ -73,81 +78,98 @@ async fn main() {
|
|||||||
)
|
)
|
||||||
.init();
|
.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");
|
.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_target_a,
|
||||||
&config.domain_checker_resolver_addr,
|
&config.domain_checker_resolver_addr,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.expect("domain checker initialization failed");
|
.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");
|
.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_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");
|
.expect("domain acme store initialization failed");
|
||||||
|
|
||||||
// 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
|
||||||
// settings.
|
// settings.
|
||||||
let domain_acme_contact_email = config.domain_acme_contact_email.unwrap();
|
let domain_acme_contact_email = config.domain_acme_contact_email.unwrap();
|
||||||
|
|
||||||
Some(
|
let domain_acme_manager = domiply::domain::acme::manager::new(
|
||||||
domani::domain::acme::manager::new(domain_acme_store, &domain_acme_contact_email)
|
domain_acme_store.clone(),
|
||||||
.await
|
&domain_acme_contact_email,
|
||||||
.expect("domain acme manager initialization failed"),
|
|
||||||
)
|
)
|
||||||
|
.await
|
||||||
|
.expect("domain acme manager initialization failed");
|
||||||
|
|
||||||
|
Some(HTTPSParams {
|
||||||
|
https_listen_addr,
|
||||||
|
domain_acme_store,
|
||||||
|
domain_acme_manager,
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut task_stack = domani::util::TaskStack::new();
|
let domain_manager = domiply::domain::manager::new(
|
||||||
|
|
||||||
let domain_manager = domani::domain::manager::new(
|
|
||||||
&mut task_stack,
|
|
||||||
origin_store,
|
origin_store,
|
||||||
domain_config_store,
|
domain_config_store,
|
||||||
domain_checker,
|
domain_checker,
|
||||||
domain_acme_manager,
|
https_params.as_ref().map(|p| p.domain_acme_manager.clone()),
|
||||||
);
|
);
|
||||||
|
|
||||||
let _ = domani::service::http::new(
|
let domain_manager = sync::Arc::new(domain_manager);
|
||||||
&mut task_stack,
|
|
||||||
|
{
|
||||||
|
let (http_service, http_service_task_set) = domiply::service::http::new(
|
||||||
domain_manager.clone(),
|
domain_manager.clone(),
|
||||||
config.domain_checker_target_a,
|
config.domain_checker_target_a,
|
||||||
config.passphrase,
|
config.passphrase,
|
||||||
config.http_listen_addr.clone(),
|
config.http_listen_addr.clone(),
|
||||||
config.http_domain.clone(),
|
config.http_domain.clone(),
|
||||||
config
|
https_params.map(|p| domiply::service::http::HTTPSParams {
|
||||||
.https_listen_addr
|
listen_addr: p.https_listen_addr,
|
||||||
.map(|listen_addr| domani::service::http::HTTPSParams {
|
cert_resolver: domiply::domain::acme::resolver::new(p.domain_acme_store),
|
||||||
listen_addr,
|
|
||||||
cert_resolver: domain_manager.clone(),
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut signals =
|
canceller.cancelled().await;
|
||||||
Signals::new(signal_hook::consts::TERM_SIGNALS).expect("initializing signals failed");
|
|
||||||
|
|
||||||
if (signals.next().await).is_some() {
|
domiply::service::http::stop(http_service, http_service_task_set).await;
|
||||||
log::info!("Gracefully shutting down...");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tokio::spawn(async move {
|
sync::Arc::into_inner(domain_manager)
|
||||||
if (signals.next().await).is_some() {
|
.unwrap()
|
||||||
log::warn!("Forcefully shutting down");
|
|
||||||
std::process::exit(1);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
task_stack
|
|
||||||
.stop()
|
.stop()
|
||||||
.await
|
.await
|
||||||
.expect("failed to stop all background tasks");
|
.expect("domain manager failed to shutdown cleanly");
|
||||||
|
|
||||||
log::info!("Graceful shutdown complete");
|
log::info!("Graceful shutdown complete");
|
||||||
}
|
}
|
||||||
|
@ -71,9 +71,9 @@ struct Store {
|
|||||||
origins: sync::RwLock<collections::HashMap<origin::Descr, sync::Arc<Origin>>>,
|
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)?;
|
fs::create_dir_all(&dir_path)?;
|
||||||
Ok(Box::new(Store {
|
Ok(sync::Arc::new(Store {
|
||||||
dir_path,
|
dir_path,
|
||||||
sync_guard: sync::Mutex::new(collections::HashMap::new()),
|
sync_guard: sync::Mutex::new(collections::HashMap::new()),
|
||||||
origins: sync::RwLock::new(collections::HashMap::new()),
|
origins: sync::RwLock::new(collections::HashMap::new()),
|
||||||
|
@ -27,14 +27,13 @@ pub struct HTTPSParams {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn new(
|
pub fn new(
|
||||||
task_stack: &mut util::TaskStack<unexpected::Error>,
|
|
||||||
domain_manager: sync::Arc<dyn domain::manager::Manager>,
|
domain_manager: sync::Arc<dyn domain::manager::Manager>,
|
||||||
target_a: net::Ipv4Addr,
|
target_a: net::Ipv4Addr,
|
||||||
passphrase: String,
|
passphrase: String,
|
||||||
http_listen_addr: net::SocketAddr,
|
http_listen_addr: net::SocketAddr,
|
||||||
http_domain: domain::Name,
|
http_domain: domain::Name,
|
||||||
https_params: Option<HTTPSParams>,
|
https_params: Option<HTTPSParams>,
|
||||||
) -> sync::Arc<Service> {
|
) -> (sync::Arc<Service>, util::TaskSet<unexpected::Error>) {
|
||||||
let service = sync::Arc::new(Service {
|
let service = sync::Arc::new(Service {
|
||||||
domain_manager: domain_manager.clone(),
|
domain_manager: domain_manager.clone(),
|
||||||
target_a,
|
target_a,
|
||||||
@ -43,7 +42,9 @@ pub fn new(
|
|||||||
handlebars: tpl::get(),
|
handlebars: tpl::get(),
|
||||||
});
|
});
|
||||||
|
|
||||||
task_stack.push_spawn(|canceller| {
|
let task_set = util::TaskSet::new();
|
||||||
|
|
||||||
|
task_set.spawn(|canceller| {
|
||||||
tasks::listen_http(
|
tasks::listen_http(
|
||||||
service.clone(),
|
service.clone(),
|
||||||
canceller,
|
canceller,
|
||||||
@ -53,7 +54,7 @@ pub fn new(
|
|||||||
});
|
});
|
||||||
|
|
||||||
if let Some(https_params) = https_params {
|
if let Some(https_params) = https_params {
|
||||||
task_stack.push_spawn(|canceller| {
|
task_set.spawn(|canceller| {
|
||||||
tasks::listen_https(
|
tasks::listen_https(
|
||||||
service.clone(),
|
service.clone(),
|
||||||
canceller,
|
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())
|
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)]
|
#[derive(Serialize)]
|
||||||
@ -378,7 +388,7 @@ impl<'svc> Service {
|
|||||||
return self.serve_origin(domain, req.uri().path());
|
return self.serve_origin(domain, req.uri().path());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Serve main domani site
|
// Serve main domiply site
|
||||||
|
|
||||||
if method == Method::GET && path.starts_with("/static/") {
|
if method == Method::GET && path.starts_with("/static/") {
|
||||||
return self.render(200, path, ());
|
return self.render(200, path, ());
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
<meta charset="UTF-8" />
|
<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" />
|
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||||
|
|
||||||
@ -28,7 +28,7 @@
|
|||||||
<body>
|
<body>
|
||||||
|
|
||||||
<header>
|
<header>
|
||||||
<h1><a href="/">Domani</a></h1>
|
<h1><a href="/">Domiply</a></h1>
|
||||||
<blockquote>The universal, zero-authentication hosting service</blockquote>
|
<blockquote>The universal, zero-authentication hosting service</blockquote>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
@ -5,19 +5,19 @@
|
|||||||
{{# if data.config }}
|
{{# if data.config }}
|
||||||
|
|
||||||
<p>Your domain <code>{{ data.domain }}</code> is already configured with
|
<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>
|
you will need to hit the "Next" button to complete the update.</p>
|
||||||
|
|
||||||
{{ else }}
|
{{ 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 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
|
to serve your domain. When you update the given branch, your domain will be
|
||||||
automatically updated too!</p>
|
automatically updated too!</p>
|
||||||
|
|
||||||
{{/if}}
|
{{/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>
|
repos.</em></p>
|
||||||
|
|
||||||
<form method="GET" action="/domain_init.html">
|
<form method="GET" action="/domain_init.html">
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<h2>Configure DNS</h2>
|
<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>
|
are two entries you will need to add:</p>
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
@ -9,7 +9,7 @@ are two entries you will need to add:</p>
|
|||||||
<code>{{ data.target_a }}</code>
|
<code>{{ data.target_a }}</code>
|
||||||
</li>
|
</li>
|
||||||
<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>
|
<code>{{ data.challenge_token }}</code>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<h2>All Domains</h2>
|
<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>
|
serving</p>
|
||||||
|
|
||||||
<ul>
|
<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
|
account needed. Just input your desired backend, add two entries to your DNS
|
||||||
server, and you're done!</p>
|
server, and you're done!</p>
|
||||||
|
|
||||||
<p><strong>YOU SHOULD NOT USE THIS FOR ANYTHING YOU CARE ABOUT AT THIS
|
<p><strong>YOU SHOULD NOT USE THIS FOR ANYTHING YOU CARE ABOUT AT THIS
|
||||||
TIME.</strong></p>
|
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>
|
but will continue to be expanded as development time permits.</p>
|
||||||
|
|
||||||
<h2>Get Started</h2>
|
<h2>Get Started</h2>
|
||||||
@ -30,20 +30,20 @@ been set up.</p>
|
|||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="/domains.html">List all existing domains</a></li>
|
<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>
|
<li><a href="mailto:me@mediocregopher.com">Report a Bug</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<h2>About</h2>
|
<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
|
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
|
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>
|
the internet, the way it was always intended.</p>
|
||||||
|
|
||||||
<h2>Roadmap</h2>
|
<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>
|
planned but not yet implemented:</p>
|
||||||
|
|
||||||
<ul>
|
<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;
|
use tokio_util::sync::CancellationToken;
|
||||||
|
|
||||||
pub fn open_file(path: &path::Path) -> io::Result<Option<fs::File>> {
|
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 TaskSet<E>
|
||||||
|
|
||||||
pub struct TaskStack<E>
|
|
||||||
where
|
where
|
||||||
E: error::Error + Send + 'static,
|
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
|
where
|
||||||
E: error::Error + Send + 'static,
|
E: error::Error + Send + 'static,
|
||||||
{
|
{
|
||||||
pub fn new() -> TaskStack<E> {
|
pub fn new() -> TaskSet<E> {
|
||||||
TaskStack {
|
TaskSet {
|
||||||
wait_group: Vec::new(),
|
canceller: CancellationToken::new(),
|
||||||
|
wait_group: FuturesUnordered::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// push adds the given Future to the stack, to be executed once stop is called.
|
pub fn spawn<F, Fut>(&self, mut f: F)
|
||||||
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)
|
|
||||||
where
|
where
|
||||||
Fut: futures::Future<Output = Result<(), E>> + Send + 'static,
|
Fut: futures::Future<Output = Result<(), E>> + Send + 'static,
|
||||||
F: FnMut(CancellationToken) -> Fut,
|
F: FnMut(CancellationToken) -> Fut,
|
||||||
{
|
{
|
||||||
let canceller = CancellationToken::new();
|
let canceller = self.canceller.clone();
|
||||||
let handle = tokio::spawn(f(canceller.clone()));
|
let handle = tokio::spawn(f(canceller));
|
||||||
self.push(async move {
|
self.wait_group.push(handle);
|
||||||
canceller.cancel();
|
|
||||||
handle.await.expect("failed to join task")
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// stop will process all operations which have been pushed onto the stack in the reverse order
|
pub async fn stop(self) -> Vec<E> {
|
||||||
/// they were pushed.
|
self.canceller.cancel();
|
||||||
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();
|
|
||||||
|
|
||||||
for fut in self.wait_group {
|
let mut res = Vec::new();
|
||||||
if let Err(err) = fut.await {
|
for f in self.wait_group {
|
||||||
return Err(err);
|
if let Err(err) = f.await.expect("task failed") {
|
||||||
|
res.push(err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
res
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user