diff --git a/.dev-config.yml b/.dev-config.yml index bc9fa6d..8557b56 100644 --- a/.dev-config.yml +++ b/.dev-config.yml @@ -5,7 +5,7 @@ domain: service: passphrase: foobar dns_records: - - type: A + - kind: A addr: 127.0.0.1 - - type: AAAA + - kind: AAAA addr: ::1 diff --git a/README.md b/README.md index df733f9..ceab2cc 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,18 @@ 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 @@ -66,14 +78,14 @@ service: # A CNAME record with the primary_domain of this server is automatically # included. dns_records: - #- type: A + #- kind: A # addr: 127.0.0.1 - #- type: AAAA + #- kind: AAAA # addr: ::1 # NOTE that the name given here must resolve to the Domani server. - #- type: CNAME + #- kind: CNAME # name: domain.com # The domain name which will be used to serve the web interface of Domani. If @@ -126,5 +138,6 @@ 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 diff --git a/src/domain/config.rs b/src/domain/config.rs index 65855f9..22352f1 100644 --- a/src/domain/config.rs +++ b/src/domain/config.rs @@ -1,7 +1,9 @@ -use std::{net, path, str::FromStr}; +use std::{collections, 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() } @@ -25,10 +27,19 @@ pub struct ConfigACME { pub contact_email: String, } +#[derive(Deserialize)] +pub struct BuiltinDomain { + #[serde(flatten)] + pub domain: domain::Domain, + + pub public: bool, +} + #[derive(Deserialize)] pub struct Config { pub store_dir_path: path::PathBuf, #[serde(default)] pub dns: ConfigDNS, pub acme: Option, + pub builtins: collections::HashMap, } diff --git a/src/domain/manager.rs b/src/domain/manager.rs index b122c5c..4fbc828 100644 --- a/src/domain/manager.rs +++ b/src/domain/manager.rs @@ -80,6 +80,9 @@ impl From for SyncError { #[derive(thiserror::Error, Debug)] pub enum SyncWithConfigError { + #[error("cannot call SyncWithConfig on builtin domain")] + BuiltinDomain, + #[error("invalid url")] InvalidURL, @@ -127,6 +130,7 @@ impl From for SyncWithConfigError { impl From for SyncWithConfigError { fn from(e: store::SetError) -> SyncWithConfigError { match e { + store::SetError::BuiltinDomain => SyncWithConfigError::BuiltinDomain, store::SetError::Unexpected(e) => SyncWithConfigError::Unexpected(e), } } diff --git a/src/domain/name.rs b/src/domain/name.rs index 4cbec1c..d74c6a4 100644 --- a/src/domain/name.rs +++ b/src/domain/name.rs @@ -1,5 +1,5 @@ use std::str::FromStr; -use std::{cmp, fmt}; +use std::{cmp, fmt, hash}; 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 { - inner: trust_dns_rr::Name, + rr: trust_dns_rr::Name, utf8_str: String, } @@ -17,7 +17,7 @@ impl Name { } pub fn as_rr(&self) -> &trust_dns_rr::Name { - &self.inner + &self.rr } } @@ -36,13 +36,21 @@ impl FromStr for Name { n.set_fqdn(true); - Ok(Name { inner: n, utf8_str }) + Ok(Name { rr: n, utf8_str }) } } impl cmp::PartialEq for Name { fn eq(&self, other: &Self) -> bool { - self.inner == other.inner + self.rr == other.rr + } +} + +impl cmp::Eq for Name {} + +impl hash::Hash for Name { + fn hash(&self, state: &mut H) { + self.rr.hash(state); } } diff --git a/src/domain/store.rs b/src/domain/store.rs index 33deb91..bbdf9a6 100644 --- a/src/domain/store.rs +++ b/src/domain/store.rs @@ -1,6 +1,4 @@ -use std::path; -use std::str::FromStr; -use std::{fs, io}; +use std::{collections, fs, io, path, str::FromStr}; use crate::domain; use crate::error::unexpected::{self, Intoable, Mappable}; @@ -16,6 +14,9 @@ pub enum GetError { #[derive(thiserror::Error, Debug)] pub enum SetError { + #[error("cannot call set on builtin domain")] + BuiltinDomain, + #[error(transparent)] Unexpected(#[from] unexpected::Error), } @@ -96,6 +97,51 @@ impl Store for FSStore { } } +pub struct StoreWithBuiltin { + inner: S, + domains: collections::HashMap, +} + +impl StoreWithBuiltin { + pub fn new( + inner: S, + builtin_domains: collections::HashMap, + ) -> StoreWithBuiltin { + StoreWithBuiltin { + inner, + domains: builtin_domains, + } + } +} + +impl Store for StoreWithBuiltin { + fn get(&self, domain: &domain::Name) -> Result { + if let Some(domain) = self.domains.get(domain) { + return Ok(domain.domain.clone()); + } + self.inner.get(domain) + } + + fn set(&self, domain: &domain::Name, config: &domain::Domain) -> Result<(), SetError> { + if self.domains.get(domain).is_some() { + return Err(SetError::BuiltinDomain); + } + self.inner.set(domain, config) + } + + fn all_domains(&self) -> Result, unexpected::Error> { + let inner_domains = self.inner.all_domains()?; + let mut domains: Vec = 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, *}; diff --git a/src/main.rs b/src/main.rs index 124bb0c..ca78e00 100644 --- a/src/main.rs +++ b/src/main.rs @@ -89,10 +89,13 @@ async fn main() { .await .expect("domain checker initialization failed"); - let domain_config_store = + let domain_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 @@ -121,7 +124,7 @@ async fn main() { let domain_manager = domani::domain::manager::ManagerImpl::new( &mut task_stack, origin_store, - domain_config_store, + domain_store, domain_checker, domain_acme_manager, ); diff --git a/src/origin/descr.rs b/src/origin/descr.rs index 5b78c9f..6e0acc8 100644 --- a/src/origin/descr.rs +++ b/src/origin/descr.rs @@ -3,6 +3,7 @@ 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 }, diff --git a/src/service.rs b/src/service.rs index 984ac3b..af5f4f1 100644 --- a/src/service.rs +++ b/src/service.rs @@ -10,7 +10,7 @@ fn default_primary_domain() -> domain::Name { } #[derive(Serialize, Deserialize, Clone, PartialEq)] -#[serde(tag = "type")] +#[serde(tag = "kind")] pub enum ConfigDNSRecord { A { addr: net::Ipv4Addr }, AAAA { addr: net::Ipv6Addr }, diff --git a/src/service/http.rs b/src/service/http.rs index 45c22ec..e8bbb3d 100644 --- a/src/service/http.rs +++ b/src/service/http.rs @@ -302,6 +302,7 @@ impl<'svc> Service { let error_msg = match sync_result { Ok(_) => None, + Err(domain::manager::SyncWithConfigError::BuiltinDomain) => Some("This domain is not able to be configured, please contact the server administrator.".to_string()), 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()),