Compare commits
No commits in common. "a917f32f044ea6d147887eb63a143b961fd2268e" and "03428cef021c03b4daef9b34f27952a85150c0ca" have entirely different histories.
a917f32f04
...
03428cef02
@ -3,11 +3,9 @@ origin:
|
||||
domain:
|
||||
store_dir_path: /tmp/domani_dev_env/domain
|
||||
service:
|
||||
http:
|
||||
form_method: GET
|
||||
passphrase: foobar
|
||||
dns_records:
|
||||
- kind: A
|
||||
- type: A
|
||||
addr: 127.0.0.1
|
||||
- kind: AAAA
|
||||
- type: AAAA
|
||||
addr: ::1
|
||||
|
12
Cargo.lock
generated
12
Cargo.lock
generated
@ -466,7 +466,6 @@ dependencies = [
|
||||
"hex",
|
||||
"http",
|
||||
"hyper",
|
||||
"hyper-reverse-proxy",
|
||||
"log",
|
||||
"mime_guess",
|
||||
"mockall",
|
||||
@ -1539,17 +1538,6 @@ dependencies = [
|
||||
"want",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-reverse-proxy"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cc1af9b1b483fb9f33bd1cda26b35eacf902f0d116fcf0d56075ea5e5923b935"
|
||||
dependencies = [
|
||||
"hyper",
|
||||
"lazy_static",
|
||||
"unicase",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-rustls"
|
||||
version = "0.24.1"
|
||||
|
@ -44,4 +44,3 @@ env_logger = "0.10.0"
|
||||
serde_yaml = "0.9.22"
|
||||
rand = "0.8.5"
|
||||
reqwest = "0.11.18"
|
||||
hyper-reverse-proxy = "0.5.1"
|
||||
|
19
README.md
19
README.md
@ -53,18 +53,6 @@ domain:
|
||||
# renewed.
|
||||
#contact_email: REQUIRED if service.http.https_addr is set
|
||||
|
||||
# builtins are domains whose configuration is built into domani. These domains
|
||||
# are not able to be configured via the web interface, and will be hidden from
|
||||
# it unless the `public` key is set to true.
|
||||
#builtins:
|
||||
|
||||
# An example built-in domain backed by a git repo.
|
||||
#example.com:
|
||||
# kind: git
|
||||
# url: "https://somewhere.com/some/repo.git"
|
||||
# branch: main
|
||||
# public: false
|
||||
|
||||
service:
|
||||
|
||||
# Passphrase which must be given by users who are configuring new domains via
|
||||
@ -78,14 +66,14 @@ service:
|
||||
# A CNAME record with the primary_domain of this server is automatically
|
||||
# included.
|
||||
dns_records:
|
||||
#- kind: A
|
||||
#- type: A
|
||||
# addr: 127.0.0.1
|
||||
|
||||
#- kind: AAAA
|
||||
#- type: AAAA
|
||||
# addr: ::1
|
||||
|
||||
# NOTE that the name given here must resolve to the Domani server.
|
||||
#- kind: CNAME
|
||||
#- type: CNAME
|
||||
# name: domain.com
|
||||
|
||||
# The domain name which will be used to serve the web interface of Domani. If
|
||||
@ -138,6 +126,5 @@ Within the shell which opens you can do `cargo run` to start a local instance.
|
||||
* Support for more backends than just git repositories, including:
|
||||
* IPFS/IPNS
|
||||
* Alternative URLs (reverse proxy)
|
||||
* Small static files (e.g. for well-knowns)
|
||||
* Google Drive
|
||||
* Dropbox
|
||||
|
@ -18,11 +18,11 @@ use sha2::{Digest, Sha256};
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
|
||||
/// Defines how a domain will behave when it is accessed. These are configured by the owner of the
|
||||
/// domain during setup.
|
||||
pub struct Settings {
|
||||
pub struct Domain {
|
||||
pub origin_descr: origin::Descr,
|
||||
}
|
||||
|
||||
impl Settings {
|
||||
impl Domain {
|
||||
pub fn hash(&self) -> Result<String, unexpected::Error> {
|
||||
let mut h = Sha256::new();
|
||||
serde_json::to_writer(&mut h, self).or_unexpected()?;
|
||||
|
@ -1,9 +1,7 @@
|
||||
use std::{collections, net, path, str::FromStr};
|
||||
use std::{net, path, str::FromStr};
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::domain;
|
||||
|
||||
fn default_resolver_addr() -> net::SocketAddr {
|
||||
net::SocketAddr::from_str("1.1.1.1:53").unwrap()
|
||||
}
|
||||
@ -27,20 +25,10 @@ pub struct ConfigACME {
|
||||
pub contact_email: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct BuiltinDomain {
|
||||
#[serde(flatten)]
|
||||
pub settings: domain::Settings,
|
||||
|
||||
pub public: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct Config {
|
||||
pub store_dir_path: path::PathBuf,
|
||||
#[serde(default)]
|
||||
pub dns: ConfigDNS,
|
||||
pub acme: Option<ConfigACME>,
|
||||
#[serde(default)]
|
||||
pub builtins: collections::HashMap<domain::Name, BuiltinDomain>,
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ use std::sync;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum GetSettingsError {
|
||||
pub enum GetConfigError {
|
||||
#[error("not found")]
|
||||
NotFound,
|
||||
|
||||
@ -15,11 +15,11 @@ pub enum GetSettingsError {
|
||||
Unexpected(#[from] unexpected::Error),
|
||||
}
|
||||
|
||||
impl From<store::GetError> for GetSettingsError {
|
||||
fn from(e: store::GetError) -> GetSettingsError {
|
||||
impl From<store::GetError> for GetConfigError {
|
||||
fn from(e: store::GetError) -> GetConfigError {
|
||||
match e {
|
||||
store::GetError::NotFound => GetSettingsError::NotFound,
|
||||
store::GetError::Unexpected(e) => GetSettingsError::Unexpected(e),
|
||||
store::GetError::NotFound => GetConfigError::NotFound,
|
||||
store::GetError::Unexpected(e) => GetConfigError::Unexpected(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -32,9 +32,6 @@ pub enum GetFileError {
|
||||
#[error("file not found")]
|
||||
FileNotFound,
|
||||
|
||||
#[error("origin is of kind proxy")]
|
||||
OriginIsProxy { url: String },
|
||||
|
||||
#[error(transparent)]
|
||||
Unexpected(#[from] unexpected::Error),
|
||||
}
|
||||
@ -82,10 +79,7 @@ impl From<store::GetError> for SyncError {
|
||||
}
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum SyncWithSettingsError {
|
||||
#[error("cannot call SyncWithSettings on builtin domain")]
|
||||
BuiltinDomain,
|
||||
|
||||
pub enum SyncWithConfigError {
|
||||
#[error("invalid url")]
|
||||
InvalidURL,
|
||||
|
||||
@ -105,36 +99,35 @@ pub enum SyncWithSettingsError {
|
||||
Unexpected(#[from] unexpected::Error),
|
||||
}
|
||||
|
||||
impl From<origin::SyncError> for SyncWithSettingsError {
|
||||
fn from(e: origin::SyncError) -> SyncWithSettingsError {
|
||||
impl From<origin::SyncError> for SyncWithConfigError {
|
||||
fn from(e: origin::SyncError) -> SyncWithConfigError {
|
||||
match e {
|
||||
origin::SyncError::InvalidURL => SyncWithSettingsError::InvalidURL,
|
||||
origin::SyncError::InvalidBranchName => SyncWithSettingsError::InvalidBranchName,
|
||||
origin::SyncError::AlreadyInProgress => SyncWithSettingsError::AlreadyInProgress,
|
||||
origin::SyncError::Unexpected(e) => SyncWithSettingsError::Unexpected(e),
|
||||
origin::SyncError::InvalidURL => SyncWithConfigError::InvalidURL,
|
||||
origin::SyncError::InvalidBranchName => SyncWithConfigError::InvalidBranchName,
|
||||
origin::SyncError::AlreadyInProgress => SyncWithConfigError::AlreadyInProgress,
|
||||
origin::SyncError::Unexpected(e) => SyncWithConfigError::Unexpected(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<checker::CheckDomainError> for SyncWithSettingsError {
|
||||
fn from(e: checker::CheckDomainError) -> SyncWithSettingsError {
|
||||
impl From<checker::CheckDomainError> for SyncWithConfigError {
|
||||
fn from(e: checker::CheckDomainError) -> SyncWithConfigError {
|
||||
match e {
|
||||
checker::CheckDomainError::ServiceDNSRecordsNotSet => {
|
||||
SyncWithSettingsError::ServiceDNSRecordsNotSet
|
||||
SyncWithConfigError::ServiceDNSRecordsNotSet
|
||||
}
|
||||
checker::CheckDomainError::ChallengeTokenNotSet => {
|
||||
SyncWithSettingsError::ChallengeTokenNotSet
|
||||
SyncWithConfigError::ChallengeTokenNotSet
|
||||
}
|
||||
checker::CheckDomainError::Unexpected(e) => SyncWithSettingsError::Unexpected(e),
|
||||
checker::CheckDomainError::Unexpected(e) => SyncWithConfigError::Unexpected(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<store::SetError> for SyncWithSettingsError {
|
||||
fn from(e: store::SetError) -> SyncWithSettingsError {
|
||||
impl From<store::SetError> for SyncWithConfigError {
|
||||
fn from(e: store::SetError) -> SyncWithConfigError {
|
||||
match e {
|
||||
store::SetError::BuiltinDomain => SyncWithSettingsError::BuiltinDomain,
|
||||
store::SetError::Unexpected(e) => SyncWithSettingsError::Unexpected(e),
|
||||
store::SetError::Unexpected(e) => SyncWithConfigError::Unexpected(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -143,7 +136,7 @@ pub type GetAcmeHttp01ChallengeKeyError = acme::manager::GetHttp01ChallengeKeyEr
|
||||
|
||||
//#[mockall::automock]
|
||||
pub trait Manager: Sync + Send + rustls::server::ResolvesServerCert {
|
||||
fn get_settings(&self, domain: &domain::Name) -> Result<domain::Settings, GetSettingsError>;
|
||||
fn get_config(&self, domain: &domain::Name) -> Result<domain::Domain, GetConfigError>;
|
||||
|
||||
fn get_file<'store>(
|
||||
&'store self,
|
||||
@ -156,11 +149,11 @@ pub trait Manager: Sync + Send + rustls::server::ResolvesServerCert {
|
||||
domain: domain::Name,
|
||||
) -> util::BoxFuture<'mgr, Result<(), unexpected::Error>>;
|
||||
|
||||
fn sync_with_settings<'mgr>(
|
||||
fn sync_with_config<'mgr>(
|
||||
&'mgr self,
|
||||
domain: domain::Name,
|
||||
settings: domain::Settings,
|
||||
) -> util::BoxFuture<'mgr, Result<(), SyncWithSettingsError>>;
|
||||
config: domain::Domain,
|
||||
) -> util::BoxFuture<'mgr, Result<(), SyncWithConfigError>>;
|
||||
|
||||
fn get_acme_http01_challenge_key(
|
||||
&self,
|
||||
@ -238,7 +231,7 @@ impl ManagerImpl {
|
||||
}
|
||||
|
||||
impl Manager for ManagerImpl {
|
||||
fn get_settings(&self, domain: &domain::Name) -> Result<domain::Settings, GetSettingsError> {
|
||||
fn get_config(&self, domain: &domain::Name) -> Result<domain::Domain, GetConfigError> {
|
||||
Ok(self.domain_store.get(domain)?)
|
||||
}
|
||||
|
||||
@ -248,11 +241,6 @@ impl Manager for ManagerImpl {
|
||||
path: &str,
|
||||
) -> Result<util::BoxByteStream, GetFileError> {
|
||||
let config = self.domain_store.get(domain)?;
|
||||
|
||||
if let origin::Descr::Proxy { url } = config.origin_descr {
|
||||
return Err(GetFileError::OriginIsProxy { url });
|
||||
}
|
||||
|
||||
let f = self.origin_store.get_file(&config.origin_descr, path)?;
|
||||
Ok(f)
|
||||
}
|
||||
@ -270,21 +258,23 @@ impl Manager for ManagerImpl {
|
||||
})
|
||||
}
|
||||
|
||||
fn sync_with_settings<'mgr>(
|
||||
fn sync_with_config<'mgr>(
|
||||
&'mgr self,
|
||||
domain: domain::Name,
|
||||
settings: domain::Settings,
|
||||
) -> util::BoxFuture<'mgr, Result<(), SyncWithSettingsError>> {
|
||||
config: domain::Domain,
|
||||
) -> util::BoxFuture<'mgr, Result<(), SyncWithConfigError>> {
|
||||
Box::pin(async move {
|
||||
let hash = settings
|
||||
let config_hash = config
|
||||
.hash()
|
||||
.or_unexpected_while("calculating config hash")?;
|
||||
|
||||
self.domain_checker.check_domain(&domain, &hash).await?;
|
||||
self.domain_checker
|
||||
.check_domain(&domain, &config_hash)
|
||||
.await?;
|
||||
|
||||
self.origin_store.sync(&settings.origin_descr)?;
|
||||
self.origin_store.sync(&config.origin_descr)?;
|
||||
|
||||
self.domain_store.set(&domain, &settings)?;
|
||||
self.domain_store.set(&domain, &config)?;
|
||||
|
||||
self.sync_cert(domain).await?;
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
use std::str::FromStr;
|
||||
use std::{cmp, fmt, hash};
|
||||
use std::{cmp, fmt};
|
||||
|
||||
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
|
||||
use trust_dns_client::rr as trust_dns_rr;
|
||||
@ -7,7 +7,7 @@ use trust_dns_client::rr as trust_dns_rr;
|
||||
#[derive(Debug, Clone)]
|
||||
/// Validated representation of a domain name
|
||||
pub struct Name {
|
||||
rr: trust_dns_rr::Name,
|
||||
inner: trust_dns_rr::Name,
|
||||
utf8_str: String,
|
||||
}
|
||||
|
||||
@ -17,7 +17,7 @@ impl Name {
|
||||
}
|
||||
|
||||
pub fn as_rr(&self) -> &trust_dns_rr::Name {
|
||||
&self.rr
|
||||
&self.inner
|
||||
}
|
||||
}
|
||||
|
||||
@ -36,21 +36,13 @@ impl FromStr for Name {
|
||||
|
||||
n.set_fqdn(true);
|
||||
|
||||
Ok(Name { rr: n, utf8_str })
|
||||
Ok(Name { inner: n, utf8_str })
|
||||
}
|
||||
}
|
||||
|
||||
impl cmp::PartialEq for Name {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.rr == other.rr
|
||||
}
|
||||
}
|
||||
|
||||
impl cmp::Eq for Name {}
|
||||
|
||||
impl hash::Hash for Name {
|
||||
fn hash<H: hash::Hasher>(&self, state: &mut H) {
|
||||
self.rr.hash(state);
|
||||
self.inner == other.inner
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,6 @@
|
||||
use std::{collections, fs, io, path, str::FromStr};
|
||||
use std::path;
|
||||
use std::str::FromStr;
|
||||
use std::{fs, io};
|
||||
|
||||
use crate::domain;
|
||||
use crate::error::unexpected::{self, Intoable, Mappable};
|
||||
@ -14,17 +16,14 @@ pub enum GetError {
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum SetError {
|
||||
#[error("cannot call set on builtin domain")]
|
||||
BuiltinDomain,
|
||||
|
||||
#[error(transparent)]
|
||||
Unexpected(#[from] unexpected::Error),
|
||||
}
|
||||
|
||||
#[mockall::automock]
|
||||
pub trait Store {
|
||||
fn get(&self, domain: &domain::Name) -> Result<domain::Settings, GetError>;
|
||||
fn set(&self, domain: &domain::Name, settings: &domain::Settings) -> Result<(), SetError>;
|
||||
fn get(&self, domain: &domain::Name) -> Result<domain::Domain, GetError>;
|
||||
fn set(&self, domain: &domain::Name, config: &domain::Domain) -> Result<(), SetError>;
|
||||
fn all_domains(&self) -> Result<Vec<domain::Name>, unexpected::Error>;
|
||||
}
|
||||
|
||||
@ -40,40 +39,40 @@ impl FSStore {
|
||||
})
|
||||
}
|
||||
|
||||
fn settings_dir_path(&self, domain: &domain::Name) -> path::PathBuf {
|
||||
fn config_dir_path(&self, domain: &domain::Name) -> path::PathBuf {
|
||||
self.dir_path.join(domain.as_str())
|
||||
}
|
||||
|
||||
fn settings_file_path(&self, domain: &domain::Name) -> path::PathBuf {
|
||||
self.settings_dir_path(domain).join("settings.json")
|
||||
fn config_file_path(&self, domain: &domain::Name) -> path::PathBuf {
|
||||
self.config_dir_path(domain).join("config.json")
|
||||
}
|
||||
}
|
||||
|
||||
impl Store for FSStore {
|
||||
fn get(&self, domain: &domain::Name) -> Result<domain::Settings, GetError> {
|
||||
let path = self.settings_file_path(domain);
|
||||
let settings_file = fs::File::open(path.as_path()).map_err(|e| match e.kind() {
|
||||
fn get(&self, domain: &domain::Name) -> Result<domain::Domain, GetError> {
|
||||
let path = self.config_file_path(domain);
|
||||
let config_file = fs::File::open(path.as_path()).map_err(|e| match e.kind() {
|
||||
io::ErrorKind::NotFound => GetError::NotFound,
|
||||
_ => e
|
||||
.into_unexpected_while(format!("opening {}", path.display()))
|
||||
.into(),
|
||||
})?;
|
||||
|
||||
let settings = serde_json::from_reader(settings_file)
|
||||
let config = serde_json::from_reader(config_file)
|
||||
.map_unexpected_while(|| format!("json parsing {}", path.display()))?;
|
||||
Ok(settings)
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
fn set(&self, domain: &domain::Name, settings: &domain::Settings) -> Result<(), SetError> {
|
||||
let dir_path = self.settings_dir_path(domain);
|
||||
fn set(&self, domain: &domain::Name, config: &domain::Domain) -> Result<(), SetError> {
|
||||
let dir_path = self.config_dir_path(domain);
|
||||
fs::create_dir_all(dir_path.as_path())
|
||||
.map_unexpected_while(|| format!("creating dir {}", dir_path.display()))?;
|
||||
|
||||
let file_path = self.settings_file_path(domain);
|
||||
let settings_file = fs::File::create(file_path.as_path())
|
||||
let file_path = self.config_file_path(domain);
|
||||
let config_file = fs::File::create(file_path.as_path())
|
||||
.map_unexpected_while(|| format!("creating file {}", file_path.display()))?;
|
||||
|
||||
serde_json::to_writer(settings_file, settings)
|
||||
serde_json::to_writer(config_file, config)
|
||||
.map_unexpected_while(|| format!("writing config to {}", file_path.display()))?;
|
||||
|
||||
Ok(())
|
||||
@ -97,51 +96,6 @@ impl Store for FSStore {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct StoreWithBuiltin<S: Store> {
|
||||
inner: S,
|
||||
domains: collections::HashMap<domain::Name, domain::config::BuiltinDomain>,
|
||||
}
|
||||
|
||||
impl<S: Store> StoreWithBuiltin<S> {
|
||||
pub fn new(
|
||||
inner: S,
|
||||
builtin_domains: collections::HashMap<domain::Name, domain::config::BuiltinDomain>,
|
||||
) -> StoreWithBuiltin<S> {
|
||||
StoreWithBuiltin {
|
||||
inner,
|
||||
domains: builtin_domains,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: Store> Store for StoreWithBuiltin<S> {
|
||||
fn get(&self, domain: &domain::Name) -> Result<domain::Settings, GetError> {
|
||||
if let Some(domain) = self.domains.get(domain) {
|
||||
return Ok(domain.settings.clone());
|
||||
}
|
||||
self.inner.get(domain)
|
||||
}
|
||||
|
||||
fn set(&self, domain: &domain::Name, settings: &domain::Settings) -> Result<(), SetError> {
|
||||
if self.domains.get(domain).is_some() {
|
||||
return Err(SetError::BuiltinDomain);
|
||||
}
|
||||
self.inner.set(domain, settings)
|
||||
}
|
||||
|
||||
fn all_domains(&self) -> Result<Vec<domain::Name>, unexpected::Error> {
|
||||
let inner_domains = self.inner.all_domains()?;
|
||||
let mut domains: Vec<domain::Name> = self
|
||||
.domains
|
||||
.iter()
|
||||
.filter(|(_, v)| v.public)
|
||||
.map(|(k, _)| k.clone())
|
||||
.collect();
|
||||
domains.extend(inner_domains);
|
||||
Ok(domains)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{Store, *};
|
||||
@ -154,13 +108,13 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn basic() {
|
||||
let tmp_dir = TempDir::new("domain_store").unwrap();
|
||||
let tmp_dir = TempDir::new("domain_config_store").unwrap();
|
||||
|
||||
let store = FSStore::new(tmp_dir.path()).expect("store created");
|
||||
|
||||
let domain = domain::Name::from_str("foo.com").expect("domain parsed");
|
||||
|
||||
let settings = domain::Settings {
|
||||
let config = domain::Domain {
|
||||
origin_descr: Descr::Git {
|
||||
url: "bar".to_string(),
|
||||
branch_name: "baz".to_string(),
|
||||
@ -169,23 +123,20 @@ mod tests {
|
||||
|
||||
assert!(matches!(
|
||||
store.get(&domain),
|
||||
Err::<domain::Settings, GetError>(GetError::NotFound)
|
||||
Err::<domain::Domain, GetError>(GetError::NotFound)
|
||||
));
|
||||
|
||||
store.set(&domain, &settings).expect("set");
|
||||
assert_eq!(settings, store.get(&domain).expect("settings retrieved"));
|
||||
store.set(&domain, &config).expect("config set");
|
||||
assert_eq!(config, store.get(&domain).expect("config retrieved"));
|
||||
|
||||
let new_settings = domain::Settings {
|
||||
let new_config = domain::Domain {
|
||||
origin_descr: Descr::Git {
|
||||
url: "BAR".to_string(),
|
||||
branch_name: "BAZ".to_string(),
|
||||
},
|
||||
};
|
||||
|
||||
store.set(&domain, &new_settings).expect("set");
|
||||
assert_eq!(
|
||||
new_settings,
|
||||
store.get(&domain).expect("settings retrieved")
|
||||
);
|
||||
store.set(&domain, &new_config).expect("config set");
|
||||
assert_eq!(new_config, store.get(&domain).expect("config retrieved"));
|
||||
}
|
||||
}
|
||||
|
@ -89,13 +89,10 @@ async fn main() {
|
||||
.await
|
||||
.expect("domain checker initialization failed");
|
||||
|
||||
let domain_store =
|
||||
let domain_config_store =
|
||||
domani::domain::store::FSStore::new(&config.domain.store_dir_path.join("domains"))
|
||||
.expect("domain config store initialization failed");
|
||||
|
||||
let domain_store =
|
||||
domani::domain::store::StoreWithBuiltin::new(domain_store, config.domain.builtins);
|
||||
|
||||
let domain_acme_manager = if config.service.http.https_addr.is_some() {
|
||||
let acme_config = config
|
||||
.domain
|
||||
@ -124,7 +121,7 @@ async fn main() {
|
||||
let domain_manager = domani::domain::manager::ManagerImpl::new(
|
||||
&mut task_stack,
|
||||
origin_store,
|
||||
domain_store,
|
||||
domain_config_store,
|
||||
domain_checker,
|
||||
domain_acme_manager,
|
||||
);
|
||||
|
@ -2,7 +2,6 @@ mod config;
|
||||
mod descr;
|
||||
pub mod git;
|
||||
pub mod mux;
|
||||
pub mod proxy;
|
||||
|
||||
pub use config::*;
|
||||
pub use descr::Descr;
|
||||
|
@ -3,11 +3,9 @@ use serde::{Deserialize, Serialize};
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[serde(tag = "kind")]
|
||||
/// A unique description of an origin, from where a domain might be served.
|
||||
pub enum Descr {
|
||||
Git { url: String, branch_name: String },
|
||||
Proxy { url: String },
|
||||
}
|
||||
|
||||
impl Descr {
|
||||
@ -26,10 +24,6 @@ impl Descr {
|
||||
h_update(url);
|
||||
h_update(branch_name);
|
||||
}
|
||||
Descr::Proxy { url } => {
|
||||
h_update("proxy");
|
||||
h_update(url);
|
||||
}
|
||||
}
|
||||
|
||||
h.finalize().encode_hex::<String>()
|
||||
|
@ -56,24 +56,15 @@ impl FSStore {
|
||||
format!("origin/{branch_name}")
|
||||
}
|
||||
|
||||
fn deconstruct_descr(descr: &origin::Descr) -> (&str, &str) {
|
||||
if let origin::Descr::Git {
|
||||
ref url,
|
||||
ref branch_name,
|
||||
} = descr
|
||||
{
|
||||
(url, branch_name)
|
||||
} else {
|
||||
panic!("non git descr passed in")
|
||||
}
|
||||
}
|
||||
|
||||
fn create_repo_snapshot(
|
||||
&self,
|
||||
repo: gix::Repository,
|
||||
descr: &origin::Descr,
|
||||
) -> Result<RepoSnapshot, CreateRepoSnapshotError> {
|
||||
let (_, branch_name) = Self::deconstruct_descr(descr);
|
||||
let origin::Descr::Git {
|
||||
ref branch_name, ..
|
||||
} = descr;
|
||||
|
||||
let branch_ref = self.branch_ref(branch_name);
|
||||
|
||||
let commit_object_id = repo
|
||||
@ -157,7 +148,10 @@ impl FSStore {
|
||||
fs::create_dir_all(repo_path)
|
||||
.map_unexpected_while(|| format!("creating {}", repo_path.display()))?;
|
||||
|
||||
let (url, branch_name) = Self::deconstruct_descr(descr);
|
||||
let origin::Descr::Git {
|
||||
ref url,
|
||||
ref branch_name,
|
||||
} = descr;
|
||||
|
||||
let (repo, _) = gix::prepare_clone_bare(url.clone(), repo_path)
|
||||
.map_err(|e| match e {
|
||||
|
@ -1,50 +0,0 @@
|
||||
use crate::error::unexpected::{self, Mappable};
|
||||
use std::{net, str::FromStr};
|
||||
|
||||
// proxy is a special case because it is so tied to the underlying protocol that a request is
|
||||
// being served on, it can't be abstracted out into a simple "get_file" operation like other
|
||||
// origins.
|
||||
|
||||
pub async fn serve_http_request(
|
||||
client_ip: net::IpAddr,
|
||||
proxy_url: &str,
|
||||
mut req: hyper::Request<hyper::Body>,
|
||||
) -> unexpected::Result<hyper::Response<hyper::Body>> {
|
||||
let parsed_proxy_url =
|
||||
http::Uri::from_str(proxy_url).or_unexpected_while("parsing proxy url")?;
|
||||
|
||||
let scheme = parsed_proxy_url
|
||||
.scheme()
|
||||
.or_unexpected_while("expected a scheme of http in the proxy url")?;
|
||||
if scheme != "http" {
|
||||
return Err(unexpected::Error::from("proxy url scheme should be 'http"));
|
||||
}
|
||||
|
||||
// figure out what the host header should be, based on the host[:port] of the proxy_url
|
||||
let host = {
|
||||
let authority = parsed_proxy_url
|
||||
.authority()
|
||||
.or_unexpected_while("getting host from proxy url, there is no host")?;
|
||||
|
||||
let host_and_port;
|
||||
let mut host = authority.host();
|
||||
|
||||
if let Some(port) = authority.port() {
|
||||
host_and_port = format!("{host}:{port}");
|
||||
host = host_and_port.as_str();
|
||||
};
|
||||
|
||||
http::header::HeaderValue::from_str(host).or_unexpected()?
|
||||
};
|
||||
|
||||
req.headers_mut().insert("host", host);
|
||||
|
||||
match hyper_reverse_proxy::call(client_ip, proxy_url, req).await {
|
||||
Ok(res) => Ok(res),
|
||||
// ProxyError doesn't actually implement Error :facepalm: so we have to format the error
|
||||
// manually
|
||||
Err(e) => Err(unexpected::Error::from(
|
||||
format!("error while proxying: {e:?}").as_str(),
|
||||
)),
|
||||
}
|
||||
}
|
@ -1,5 +1,38 @@
|
||||
mod config;
|
||||
pub mod http;
|
||||
mod util;
|
||||
|
||||
pub use config::*;
|
||||
use crate::domain;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{net, str::FromStr};
|
||||
|
||||
fn default_primary_domain() -> domain::Name {
|
||||
domain::Name::from_str("localhost").unwrap()
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, PartialEq)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum ConfigDNSRecord {
|
||||
A { addr: net::Ipv4Addr },
|
||||
AAAA { addr: net::Ipv6Addr },
|
||||
CNAME { name: domain::Name },
|
||||
}
|
||||
|
||||
impl From<ConfigDNSRecord> for domain::checker::DNSRecord {
|
||||
fn from(r: ConfigDNSRecord) -> Self {
|
||||
match r {
|
||||
ConfigDNSRecord::A { addr } => Self::A(addr),
|
||||
ConfigDNSRecord::AAAA { addr } => Self::AAAA(addr),
|
||||
ConfigDNSRecord::CNAME { name } => Self::CNAME(name),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct Config {
|
||||
pub passphrase: String,
|
||||
pub dns_records: Vec<ConfigDNSRecord>,
|
||||
#[serde(default = "default_primary_domain")]
|
||||
pub primary_domain: domain::Name,
|
||||
#[serde(default)]
|
||||
pub http: self::http::Config,
|
||||
}
|
||||
|
@ -1,35 +0,0 @@
|
||||
use crate::{domain, service};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{net, str::FromStr};
|
||||
|
||||
fn default_primary_domain() -> domain::Name {
|
||||
domain::Name::from_str("localhost").unwrap()
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, PartialEq)]
|
||||
#[serde(tag = "kind")]
|
||||
pub enum ConfigDNSRecord {
|
||||
A { addr: net::Ipv4Addr },
|
||||
AAAA { addr: net::Ipv6Addr },
|
||||
CNAME { name: domain::Name },
|
||||
}
|
||||
|
||||
impl From<ConfigDNSRecord> for domain::checker::DNSRecord {
|
||||
fn from(r: ConfigDNSRecord) -> Self {
|
||||
match r {
|
||||
ConfigDNSRecord::A { addr } => Self::A(addr),
|
||||
ConfigDNSRecord::AAAA { addr } => Self::AAAA(addr),
|
||||
ConfigDNSRecord::CNAME { name } => Self::CNAME(name),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct Config {
|
||||
pub passphrase: String,
|
||||
pub dns_records: Vec<ConfigDNSRecord>,
|
||||
#[serde(default = "default_primary_domain")]
|
||||
pub primary_domain: domain::Name,
|
||||
#[serde(default)]
|
||||
pub http: service::http::Config,
|
||||
}
|
@ -4,15 +4,14 @@ mod tpl;
|
||||
|
||||
pub use config::*;
|
||||
|
||||
use http::request::Parts;
|
||||
use hyper::{Body, Method, Request, Response};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use std::str::FromStr;
|
||||
use std::{future, net, sync};
|
||||
use std::{future, sync};
|
||||
|
||||
use crate::error::unexpected;
|
||||
use crate::{domain, origin, service, util};
|
||||
use crate::{domain, service, util};
|
||||
|
||||
pub struct Service {
|
||||
domain_manager: sync::Arc<dyn domain::manager::Manager>,
|
||||
@ -49,7 +48,6 @@ pub fn new(
|
||||
#[derive(Serialize)]
|
||||
struct BasePresenter<'a, T> {
|
||||
page_name: &'a str,
|
||||
form_method: &'a str,
|
||||
data: T,
|
||||
}
|
||||
|
||||
@ -61,18 +59,12 @@ struct DomainGetArgs {
|
||||
#[derive(Deserialize)]
|
||||
struct DomainInitArgs {
|
||||
domain: domain::Name,
|
||||
|
||||
#[serde(flatten)]
|
||||
flat_domain_settings: service::util::FlatDomainSettings,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct DomainSyncArgs {
|
||||
domain: domain::Name,
|
||||
passphrase: String,
|
||||
|
||||
#[serde(flatten)]
|
||||
flat_domain_settings: service::util::FlatDomainSettings,
|
||||
}
|
||||
|
||||
impl<'svc> Service {
|
||||
@ -129,7 +121,6 @@ impl<'svc> Service {
|
||||
"/base.html",
|
||||
BasePresenter {
|
||||
page_name: "/error.html",
|
||||
form_method: self.config.http.form_method.as_str(),
|
||||
data: &Response { error_msg: e },
|
||||
},
|
||||
)
|
||||
@ -152,20 +143,13 @@ impl<'svc> Service {
|
||||
"/base.html",
|
||||
BasePresenter {
|
||||
page_name: name,
|
||||
form_method: self.config.http.form_method.as_str(),
|
||||
data,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
async fn serve_origin(
|
||||
&self,
|
||||
client_ip: net::IpAddr,
|
||||
domain: domain::Name,
|
||||
req: Request<Body>,
|
||||
) -> Response<Body> {
|
||||
fn serve_origin(&self, domain: domain::Name, path: &str) -> Response<Body> {
|
||||
let mut path_owned;
|
||||
let path = req.uri().path();
|
||||
|
||||
let path = match path.ends_with('/') {
|
||||
true => {
|
||||
@ -184,46 +168,20 @@ impl<'svc> Service {
|
||||
Err(domain::manager::GetFileError::FileNotFound) => {
|
||||
self.render_error_page(404, "File not found")
|
||||
}
|
||||
Err(domain::manager::GetFileError::OriginIsProxy { url }) => {
|
||||
origin::proxy::serve_http_request(client_ip, &url, req)
|
||||
.await
|
||||
.unwrap_or_else(|e| {
|
||||
self.internal_error(format!("proxying {domain} to {url}: {e}").as_str())
|
||||
})
|
||||
}
|
||||
Err(domain::manager::GetFileError::Unexpected(e)) => {
|
||||
self.internal_error(format!("failed to fetch file {path}: {e}").as_str())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn with_query_req<'a, F, In, Out>(
|
||||
&self,
|
||||
req: &'a Parts,
|
||||
body: Body,
|
||||
f: F,
|
||||
) -> Response<Body>
|
||||
async fn with_query_req<'a, F, In, Out>(&self, req: &'a Request<Body>, f: F) -> Response<Body>
|
||||
where
|
||||
In: for<'d> Deserialize<'d>,
|
||||
In: Deserialize<'a>,
|
||||
F: FnOnce(In) -> Out,
|
||||
Out: future::Future<Output = Response<Body>>,
|
||||
{
|
||||
let res = match self.config.http.form_method {
|
||||
ConfigFormMethod::GET => {
|
||||
serde_urlencoded::from_str::<In>(req.uri.query().unwrap_or(""))
|
||||
}
|
||||
ConfigFormMethod::POST => {
|
||||
let body = match hyper::body::to_bytes(body).await {
|
||||
Ok(bytes) => bytes.to_vec(),
|
||||
Err(e) => {
|
||||
return self.internal_error(format!("failed to read body: {e}").as_str())
|
||||
}
|
||||
};
|
||||
serde_urlencoded::from_bytes::<In>(body.as_ref())
|
||||
}
|
||||
};
|
||||
|
||||
match res {
|
||||
let query = req.uri().query().unwrap_or("");
|
||||
match serde_urlencoded::from_str::<In>(query) {
|
||||
Ok(args) => f(args).await,
|
||||
Err(err) => {
|
||||
self.render_error_page(400, format!("failed to parse query args: {}", err).as_str())
|
||||
@ -233,35 +191,43 @@ impl<'svc> Service {
|
||||
|
||||
fn domain_get(&self, args: DomainGetArgs) -> Response<Body> {
|
||||
#[derive(Serialize)]
|
||||
struct Data {
|
||||
struct Response {
|
||||
domain: domain::Name,
|
||||
settings: Option<domain::Settings>,
|
||||
config: Option<domain::Domain>,
|
||||
}
|
||||
|
||||
let settings = match self.domain_manager.get_settings(&args.domain) {
|
||||
Ok(settings) => Some(settings),
|
||||
Err(domain::manager::GetSettingsError::NotFound) => None,
|
||||
Err(domain::manager::GetSettingsError::Unexpected(e)) => {
|
||||
let config = match self.domain_manager.get_config(&args.domain) {
|
||||
Ok(config) => Some(config),
|
||||
Err(domain::manager::GetConfigError::NotFound) => None,
|
||||
Err(domain::manager::GetConfigError::Unexpected(e)) => {
|
||||
return self.internal_error(
|
||||
format!("retrieving settings for domain {}: {}", &args.domain, e).as_str(),
|
||||
format!(
|
||||
"retrieving configuration for domain {}: {}",
|
||||
&args.domain, e
|
||||
)
|
||||
.as_str(),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
self.render_page(
|
||||
"/domain.html",
|
||||
Data {
|
||||
Response {
|
||||
domain: args.domain,
|
||||
settings,
|
||||
config,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn domain_init(&self, args: DomainInitArgs) -> Response<Body> {
|
||||
fn domain_init(
|
||||
&self,
|
||||
args: DomainInitArgs,
|
||||
domain_config: service::util::FlatConfig,
|
||||
) -> Response<Body> {
|
||||
#[derive(Serialize)]
|
||||
struct Data<'a> {
|
||||
struct Response<'a> {
|
||||
domain: domain::Name,
|
||||
flat_domain_settings: service::util::FlatDomainSettings,
|
||||
flat_config: service::util::FlatConfig,
|
||||
dns_records: &'a [service::ConfigDNSRecord],
|
||||
challenge_token: String,
|
||||
|
||||
@ -269,19 +235,19 @@ impl<'svc> Service {
|
||||
dns_records_have_cname: bool,
|
||||
}
|
||||
|
||||
let settings: domain::Settings = match args.flat_domain_settings.try_into() {
|
||||
Ok(settings) => settings,
|
||||
let config: domain::Domain = match domain_config.try_into() {
|
||||
Ok(Some(config)) => config,
|
||||
Ok(None) => return self.render_error_page(400, "domain config is required"),
|
||||
Err(e) => {
|
||||
return self
|
||||
.render_error_page(400, format!("invalid domain settings: {e}").as_str())
|
||||
return self.render_error_page(400, format!("invalid domain config: {e}").as_str())
|
||||
}
|
||||
};
|
||||
|
||||
let settings_hash = match settings.hash() {
|
||||
let config_hash = match config.hash() {
|
||||
Ok(hash) => hash,
|
||||
Err(e) => {
|
||||
return self.internal_error(
|
||||
format!("failed to hash domain settings {settings:?}: {e}").as_str(),
|
||||
format!("failed to hash domain config {config:?}: {e}").as_str(),
|
||||
)
|
||||
}
|
||||
};
|
||||
@ -292,21 +258,13 @@ impl<'svc> Service {
|
||||
_ => false,
|
||||
});
|
||||
|
||||
let flat_domain_settings = match settings.try_into() {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
return self
|
||||
.internal_error(format!("failed to flatten domains settings: {e}").as_str())
|
||||
}
|
||||
};
|
||||
|
||||
self.render_page(
|
||||
"/domain_init.html",
|
||||
Data {
|
||||
Response {
|
||||
domain: args.domain,
|
||||
flat_domain_settings,
|
||||
flat_config: config.into(),
|
||||
dns_records: &self.config.dns_records,
|
||||
challenge_token: settings_hash,
|
||||
challenge_token: config_hash,
|
||||
|
||||
domain_is_zone_apex,
|
||||
dns_records_have_cname,
|
||||
@ -314,48 +272,50 @@ impl<'svc> Service {
|
||||
)
|
||||
}
|
||||
|
||||
async fn domain_sync(&self, args: DomainSyncArgs) -> Response<Body> {
|
||||
async fn domain_sync(
|
||||
&self,
|
||||
args: DomainSyncArgs,
|
||||
domain_config: service::util::FlatConfig,
|
||||
) -> Response<Body> {
|
||||
if args.passphrase != self.config.passphrase.as_str() {
|
||||
return self.render_error_page(401, "Incorrect passphrase");
|
||||
}
|
||||
|
||||
let settings: domain::Settings = match args.flat_domain_settings.try_into() {
|
||||
Ok(settings) => settings,
|
||||
let config: domain::Domain = match domain_config.try_into() {
|
||||
Ok(Some(config)) => config,
|
||||
Ok(None) => return self.render_error_page(400, "domain config is required"),
|
||||
Err(e) => {
|
||||
return self
|
||||
.render_error_page(400, format!("invalid domain settings: {e}").as_str())
|
||||
return self.render_error_page(400, format!("invalid domain config: {e}").as_str())
|
||||
}
|
||||
};
|
||||
|
||||
let sync_result = self
|
||||
.domain_manager
|
||||
.sync_with_settings(args.domain.clone(), settings)
|
||||
.sync_with_config(args.domain.clone(), config)
|
||||
.await;
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct Data {
|
||||
struct Response {
|
||||
domain: domain::Name,
|
||||
error_msg: Option<String>,
|
||||
}
|
||||
|
||||
let error_msg = match sync_result {
|
||||
Ok(_) => None,
|
||||
Err(domain::manager::SyncWithSettingsError::BuiltinDomain) => Some("This domain is not able to be configured, please contact the server administrator.".to_string()),
|
||||
Err(domain::manager::SyncWithSettingsError::InvalidURL) => Some("Fetching the git repository failed, please double check that you input the correct URL.".to_string()),
|
||||
Err(domain::manager::SyncWithSettingsError::InvalidBranchName) => Some("The git repository does not have a branch of the given name, please double check that you input the correct name.".to_string()),
|
||||
Err(domain::manager::SyncWithSettingsError::AlreadyInProgress) => Some("The configuration of your domain is still in progress, please refresh in a few minutes.".to_string()),
|
||||
Err(domain::manager::SyncWithSettingsError::ServiceDNSRecordsNotSet) => Some("None of the expected service DNS records were set on the domain. Please double check that you put the correct value on the record. If the value is correct, then most likely the updated records have not yet propagated. In this case you can refresh in a few minutes to try again.".to_string()),
|
||||
Err(domain::manager::SyncWithSettingsError::ChallengeTokenNotSet) => Some("The TXT record is not set correctly on the domain. Please double check that you put the correct value on the record. If the value is correct, then most likely the updated records have not yet propagated. In this case you can refresh in a few minutes to try again.".to_string()),
|
||||
Err(domain::manager::SyncWithSettingsError::Unexpected(e)) => Some(format!("An unexpected error occurred: {e}")),
|
||||
Err(domain::manager::SyncWithConfigError::InvalidURL) => Some("Fetching the git repository failed, please double check that you input the correct URL.".to_string()),
|
||||
Err(domain::manager::SyncWithConfigError::InvalidBranchName) => Some("The git repository does not have a branch of the given name, please double check that you input the correct name.".to_string()),
|
||||
Err(domain::manager::SyncWithConfigError::AlreadyInProgress) => Some("The configuration of your domain is still in progress, please refresh in a few minutes.".to_string()),
|
||||
Err(domain::manager::SyncWithConfigError::ServiceDNSRecordsNotSet) => Some("None of the expected service DNS records were set on the domain. Please double check that you put the correct value on the record. If the value is correct, then most likely the updated records have not yet propagated. In this case you can refresh in a few minutes to try again.".to_string()),
|
||||
Err(domain::manager::SyncWithConfigError::ChallengeTokenNotSet) => Some("The TXT record is not set correctly on the domain. Please double check that you put the correct value on the record. If the value is correct, then most likely the updated records have not yet propagated. In this case you can refresh in a few minutes to try again.".to_string()),
|
||||
Err(domain::manager::SyncWithConfigError::Unexpected(e)) => Some(format!("An unexpected error occurred: {e}")),
|
||||
};
|
||||
|
||||
self.render_page(
|
||||
"/domain_sync.html",
|
||||
Data {
|
||||
domain: args.domain,
|
||||
error_msg,
|
||||
},
|
||||
)
|
||||
let response = Response {
|
||||
domain: args.domain,
|
||||
error_msg,
|
||||
};
|
||||
|
||||
self.render_page("/domain_sync.html", response)
|
||||
}
|
||||
|
||||
fn domains(&self) -> Response<Body> {
|
||||
@ -379,7 +339,7 @@ impl<'svc> Service {
|
||||
self.render_page("/domains.html", Response { domains })
|
||||
}
|
||||
|
||||
async fn handle_request(&self, client_ip: net::IpAddr, req: Request<Body>) -> Response<Body> {
|
||||
async fn handle_request(&self, req: Request<Body>) -> Response<Body> {
|
||||
let maybe_host = match (
|
||||
req.headers()
|
||||
.get("Host")
|
||||
@ -393,34 +353,33 @@ impl<'svc> Service {
|
||||
}
|
||||
.and_then(|h| domain::Name::from_str(h).ok());
|
||||
|
||||
{
|
||||
let path = req.uri().path();
|
||||
let method = req.method();
|
||||
let path = req.uri().path();
|
||||
|
||||
// Serving acme challenges always takes priority. We serve them from the same store no
|
||||
// matter the domain, presumably they are cryptographically random enough that it doesn't
|
||||
// matter.
|
||||
if req.method() == Method::GET && path.starts_with("/.well-known/acme-challenge/") {
|
||||
let token = path.trim_start_matches("/.well-known/acme-challenge/");
|
||||
// Serving acme challenges always takes priority. We serve them from the same store no
|
||||
// matter the domain, presumably they are cryptographically random enough that it doesn't
|
||||
// matter.
|
||||
if method == Method::GET && path.starts_with("/.well-known/acme-challenge/") {
|
||||
let token = path.trim_start_matches("/.well-known/acme-challenge/");
|
||||
|
||||
if let Ok(key) = self.domain_manager.get_acme_http01_challenge_key(token) {
|
||||
return self.serve(200, "token.txt", key.into());
|
||||
}
|
||||
if let Ok(key) = self.domain_manager.get_acme_http01_challenge_key(token) {
|
||||
return self.serve(200, "token.txt", key.into());
|
||||
}
|
||||
}
|
||||
|
||||
// Serving domani challenges similarly takes priority.
|
||||
if req.method() == Method::GET && path == "/.well-known/domani-challenge" {
|
||||
if let Some(ref domain) = maybe_host {
|
||||
match self
|
||||
.domain_manager
|
||||
.get_domain_checker_challenge_token(domain)
|
||||
{
|
||||
Ok(Some(token)) => return self.serve(200, "token.txt", token.into()),
|
||||
Ok(None) => return self.render_error_page(404, "Token not found"),
|
||||
Err(e) => {
|
||||
return self.internal_error(
|
||||
format!("failed to get token for domain {}: {e}", domain).as_str(),
|
||||
)
|
||||
}
|
||||
// Serving domani challenges similarly takes priority.
|
||||
if method == Method::GET && path == "/.well-known/domani-challenge" {
|
||||
if let Some(ref domain) = maybe_host {
|
||||
match self
|
||||
.domain_manager
|
||||
.get_domain_checker_challenge_token(domain)
|
||||
{
|
||||
Ok(Some(token)) => return self.serve(200, "token.txt", token.into()),
|
||||
Ok(None) => return self.render_error_page(404, "Token not found"),
|
||||
Err(e) => {
|
||||
return self.internal_error(
|
||||
format!("failed to get token for domain {}: {e}", domain).as_str(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -428,38 +387,38 @@ impl<'svc> Service {
|
||||
|
||||
// If a managed domain was given then serve that from its origin
|
||||
if let Some(domain) = maybe_host {
|
||||
return self.serve_origin(client_ip, domain, req).await;
|
||||
return self.serve_origin(domain, req.uri().path());
|
||||
}
|
||||
|
||||
// Serve main domani site
|
||||
let (req, body) = req.into_parts();
|
||||
let path = req.uri.path();
|
||||
|
||||
if req.method == Method::GET && path.starts_with("/static/") {
|
||||
if method == Method::GET && path.starts_with("/static/") {
|
||||
return self.render(200, path, ());
|
||||
}
|
||||
|
||||
let config_form_method = self.config.http.form_method.as_ref();
|
||||
|
||||
match (&req.method, path) {
|
||||
match (method, path) {
|
||||
(&Method::GET, "/") | (&Method::GET, "/index.html") => {
|
||||
self.render_page("/index.html", ())
|
||||
}
|
||||
(form_method, "/domain.html") if form_method == config_form_method => {
|
||||
self.with_query_req(&req, body, |args: DomainGetArgs| async {
|
||||
self.domain_get(args)
|
||||
(&Method::GET, "/domain.html") => {
|
||||
self.with_query_req(&req, |args: DomainGetArgs| async { self.domain_get(args) })
|
||||
.await
|
||||
}
|
||||
(&Method::GET, "/domain_init.html") => {
|
||||
self.with_query_req(&req, |args: DomainInitArgs| async {
|
||||
self.with_query_req(&req, |config: service::util::FlatConfig| async {
|
||||
self.domain_init(args, config)
|
||||
})
|
||||
.await
|
||||
})
|
||||
.await
|
||||
}
|
||||
(form_method, "/domain_init.html") if form_method == config_form_method => {
|
||||
self.with_query_req(&req, body, |args: DomainInitArgs| async {
|
||||
self.domain_init(args)
|
||||
})
|
||||
.await
|
||||
}
|
||||
(form_method, "/domain_sync.html") if form_method == config_form_method => {
|
||||
self.with_query_req(&req, body, |args: DomainSyncArgs| async {
|
||||
self.domain_sync(args).await
|
||||
(&Method::GET, "/domain_sync.html") => {
|
||||
self.with_query_req(&req, |args: DomainSyncArgs| async {
|
||||
self.with_query_req(&req, |config: service::util::FlatConfig| async {
|
||||
self.domain_sync(args, config).await
|
||||
})
|
||||
.await
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
@ -5,44 +5,11 @@ fn default_http_addr() -> net::SocketAddr {
|
||||
net::SocketAddr::from_str("[::]:3030").unwrap()
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub enum ConfigFormMethod {
|
||||
GET,
|
||||
POST,
|
||||
}
|
||||
|
||||
impl ConfigFormMethod {
|
||||
pub fn as_str(&self) -> &str {
|
||||
match self {
|
||||
Self::GET => "GET",
|
||||
Self::POST => "POST",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ConfigFormMethod {
|
||||
fn default() -> Self {
|
||||
Self::POST
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<hyper::Method> for ConfigFormMethod {
|
||||
fn as_ref(&self) -> &hyper::Method {
|
||||
match self {
|
||||
Self::GET => &hyper::Method::GET,
|
||||
Self::POST => &hyper::Method::POST,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct Config {
|
||||
#[serde(default = "default_http_addr")]
|
||||
pub http_addr: net::SocketAddr,
|
||||
pub https_addr: Option<net::SocketAddr>,
|
||||
|
||||
#[serde(default)]
|
||||
pub form_method: ConfigFormMethod,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
@ -50,7 +17,6 @@ impl Default for Config {
|
||||
Self {
|
||||
http_addr: default_http_addr(),
|
||||
https_addr: None,
|
||||
form_method: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,8 +4,6 @@ use crate::service;
|
||||
use std::{convert, future, sync};
|
||||
|
||||
use futures::StreamExt;
|
||||
use hyper::server::conn::AddrStream;
|
||||
use tokio_rustls::server::TlsStream;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
pub async fn listen_http(
|
||||
@ -15,14 +13,13 @@ pub async fn listen_http(
|
||||
let addr = service.config.http.http_addr.clone();
|
||||
let primary_domain = service.config.primary_domain.clone();
|
||||
|
||||
let make_service = hyper::service::make_service_fn(move |conn: &AddrStream| {
|
||||
let make_service = hyper::service::make_service_fn(move |_| {
|
||||
let service = service.clone();
|
||||
let client_ip = conn.remote_addr().ip();
|
||||
|
||||
// Create a `Service` for responding to the request.
|
||||
let hyper_service = hyper::service::service_fn(move |req| {
|
||||
let service = service.clone();
|
||||
async move { Ok::<_, convert::Infallible>(service.handle_request(client_ip, req).await) }
|
||||
async move { Ok::<_, convert::Infallible>(service.handle_request(req).await) }
|
||||
});
|
||||
|
||||
// Return the service to hyper.
|
||||
@ -51,14 +48,13 @@ pub async fn listen_https(
|
||||
let addr = service.config.http.https_addr.unwrap().clone();
|
||||
let primary_domain = service.config.primary_domain.clone();
|
||||
|
||||
let make_service = hyper::service::make_service_fn(move |conn: &TlsStream<AddrStream>| {
|
||||
let make_service = hyper::service::make_service_fn(move |_| {
|
||||
let service = service.clone();
|
||||
let client_ip = conn.get_ref().0.remote_addr().ip();
|
||||
|
||||
// Create a `Service` for responding to the request.
|
||||
let hyper_service = hyper::service::service_fn(move |req| {
|
||||
let service = service.clone();
|
||||
async move { Ok::<_, convert::Infallible>(service.handle_request(client_ip, req).await) }
|
||||
async move { Ok::<_, convert::Infallible>(service.handle_request(req).await) }
|
||||
});
|
||||
|
||||
// Return the service to hyper.
|
||||
|
@ -2,7 +2,7 @@
|
||||
Configure New Domain
|
||||
</h2>
|
||||
|
||||
{{# if data.settings }}
|
||||
{{# 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
|
||||
@ -20,7 +20,7 @@ automatically updated too!</p>
|
||||
<p><em>In the future Domani will support more backends than just git
|
||||
repos.</em></p>
|
||||
|
||||
<form method="{{ form_method }}" action="/domain_init.html">
|
||||
<form method="GET" action="/domain_init.html">
|
||||
<input name="domain" type="hidden" value="{{ data.domain }}" />
|
||||
<input name="config_origin_descr_kind" type="hidden" value="git" />
|
||||
|
||||
@ -32,7 +32,7 @@ automatically updated too!</p>
|
||||
<input name="config_origin_descr_git_url"
|
||||
type="text"
|
||||
placeholder="https://example.com/some_repo.git"
|
||||
value="{{ data.settings.origin_descr.Git.url }}"
|
||||
value="{{ data.config.origin_descr.Git.url }}"
|
||||
required />
|
||||
</label>
|
||||
</p>
|
||||
@ -43,7 +43,7 @@ automatically updated too!</p>
|
||||
<input name="config_origin_descr_git_branch_name"
|
||||
type="text"
|
||||
placeholder="main / master / etc..."
|
||||
value="{{ data.settings.origin_descr.Git.branch_name }}"
|
||||
value="{{ data.config.origin_descr.Git.branch_name }}"
|
||||
required />
|
||||
</label>
|
||||
</p>
|
||||
|
@ -3,9 +3,9 @@
|
||||
<p>This step requires a passphrase that has been given to you by the
|
||||
administrator of the Domani server:</p>
|
||||
|
||||
<form method="{{ form_method }}" action="/domain_sync.html" id="syncForm">
|
||||
<form method="GET" action="/domain_sync.html" id="syncForm">
|
||||
<input name="domain" type="hidden" value="{{ data.domain }}" />
|
||||
{{ #each data.flat_domain_settings }}
|
||||
{{ #each data.flat_config }}
|
||||
<input name="{{ @key }}" type="hidden" value="{{ this }}" />
|
||||
{{ /each }}
|
||||
|
||||
@ -47,7 +47,7 @@ query for your domain name. It can be <strong>one or more of</strong>:</p>
|
||||
|
||||
{{ #each data.dns_records }}
|
||||
<tr>
|
||||
<td>{{ this.kind }}</td>
|
||||
<td>{{ this.type }}</td>
|
||||
<td>{{ lookup ../data "domain" }}</td>
|
||||
{{ #if this.name }}
|
||||
<td>{{ this.name }}</td>
|
||||
|
@ -13,7 +13,7 @@ server, and you're done!</p>
|
||||
<p>Input your domain name below to set it up, or to reconfigure it has already
|
||||
been set up.</p>
|
||||
|
||||
<form method="{{ form_method }}" action="/domain.html">
|
||||
<form method="get" action="/domain.html">
|
||||
|
||||
<fieldset>
|
||||
<label>
|
||||
|
@ -1,58 +1,49 @@
|
||||
use std::convert::TryFrom;
|
||||
use std::convert::{From, TryFrom};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{domain, error::unexpected, origin};
|
||||
use crate::{domain, origin};
|
||||
|
||||
#[derive(Serialize, Deserialize, Default)]
|
||||
pub struct FlatDomainSettings {
|
||||
domain_setting_origin_descr_kind: Option<String>,
|
||||
|
||||
domain_setting_origin_descr_git_url: Option<String>,
|
||||
domain_setting_origin_descr_git_branch_name: Option<String>,
|
||||
|
||||
domain_setting_origin_descr_proxy_url: Option<String>,
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct FlatConfig {
|
||||
config_origin_descr_kind: Option<String>,
|
||||
config_origin_descr_git_url: Option<String>,
|
||||
config_origin_descr_git_branch_name: Option<String>,
|
||||
}
|
||||
|
||||
impl TryFrom<FlatDomainSettings> for domain::Settings {
|
||||
impl TryFrom<FlatConfig> for Option<domain::Domain> {
|
||||
type Error = String;
|
||||
|
||||
fn try_from(v: FlatDomainSettings) -> Result<Self, Self::Error> {
|
||||
let origin_descr = match v
|
||||
.domain_setting_origin_descr_kind
|
||||
fn try_from(v: FlatConfig) -> Result<Self, Self::Error> {
|
||||
match v
|
||||
.config_origin_descr_kind
|
||||
.unwrap_or("".to_string())
|
||||
.as_str()
|
||||
{
|
||||
"git" => Ok(origin::Descr::Git {
|
||||
url: v
|
||||
.domain_setting_origin_descr_git_url
|
||||
.ok_or("missing domain_setting_origin_descr_git_url")?,
|
||||
branch_name: v
|
||||
.domain_setting_origin_descr_git_branch_name
|
||||
.ok_or("missing domain_setting_origin_descr_git_branch_name")?,
|
||||
}),
|
||||
"" => Err("missing domain_setting_origin_descr_kind".to_string()),
|
||||
_ => Err("invalid domain_setting_origin_descr_kind".to_string()),
|
||||
}?;
|
||||
|
||||
Ok(Self { origin_descr })
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<domain::Settings> for FlatDomainSettings {
|
||||
type Error = unexpected::Error;
|
||||
|
||||
fn try_from(v: domain::Settings) -> Result<Self, Self::Error> {
|
||||
match v.origin_descr {
|
||||
origin::Descr::Git { url, branch_name } => Ok(FlatDomainSettings {
|
||||
domain_setting_origin_descr_kind: Some("git".to_string()),
|
||||
domain_setting_origin_descr_git_url: Some(url),
|
||||
domain_setting_origin_descr_git_branch_name: Some(branch_name),
|
||||
..Default::default()
|
||||
}),
|
||||
origin::Descr::Proxy { .. } => Err(unexpected::Error::from(
|
||||
"proxy origins not supported for forms",
|
||||
)),
|
||||
"" => Ok(None),
|
||||
"git" => Ok(Some(domain::Domain {
|
||||
origin_descr: origin::Descr::Git {
|
||||
url: v
|
||||
.config_origin_descr_git_url
|
||||
.ok_or("config_origin_descr_git_url missing")?,
|
||||
branch_name: v
|
||||
.config_origin_descr_git_branch_name
|
||||
.ok_or("config_origin_descr_git_branch_name missing")?,
|
||||
},
|
||||
})),
|
||||
_ => Err("invalid config_origin_descr_kind".to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<domain::Domain> for FlatConfig {
|
||||
fn from(v: domain::Domain) -> Self {
|
||||
match v.origin_descr {
|
||||
origin::Descr::Git { url, branch_name } => FlatConfig {
|
||||
config_origin_descr_kind: Some("git".to_string()),
|
||||
config_origin_descr_git_url: Some(url),
|
||||
config_origin_descr_git_branch_name: Some(branch_name),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user