diff --git a/README.md b/README.md
index 19079a9..5c836a4 100644
--- a/README.md
+++ b/README.md
@@ -61,7 +61,10 @@ service:
# DNS records which users must add to their domain's DNS so that
# Domani can serve the domains. All records given must route to this Domani
- # instance. At least one record must be given.
+ # instance.
+ #
+ # A CNAME record with the primary_domain of this server is automatically
+ # included.
dns_records:
#- type: A
# addr: 127.0.0.1
@@ -69,6 +72,10 @@ service:
#- type: AAAA
# addr: ::1
+ # NOTE that the name given here must resolve to the Domani server.
+ #- type: CNAME
+ # name: domain.com
+
# The domain name which will be used to serve the web interface of Domani. If
# service.http.https_addr is enabled then an HTTPS certificate for this domain
# will be retrieved automatically.
@@ -116,9 +123,14 @@ Within the shell which opens you can do `cargo run` to start a local instance.
## Roadmap
-* Support for CNAME records
* Support for more backends than just git repositories, including:
* IPFS/IPNS
* Alternative URLs (reverse proxy)
* Google Drive
* Dropbox
+
+* Better support for dDNS servers. If a server is behind dDNS then users can
+ only add it to their domain via CNAME. If they add the CNAME (or ALIAS or
+ whatever) to the zone apex then Domani can't see that actual CNAME record (the
+ DNS server will flatten it to A records). This breaks Domani.
+
diff --git a/TODO b/TODO
deleted file mode 100644
index e69de29..0000000
diff --git a/src/domain/checker.rs b/src/domain/checker.rs
index 702957f..c8e58f6 100644
--- a/src/domain/checker.rs
+++ b/src/domain/checker.rs
@@ -24,32 +24,10 @@ pub enum CheckDomainError {
pub enum DNSRecord {
A(net::Ipv4Addr),
AAAA(net::Ipv6Addr),
+ CNAME(domain::Name),
}
impl DNSRecord {
- async fn check_aaaa(
- client: &mut AsyncClient,
- domain: &trust_dns_client::rr::Name,
- addr: &net::Ipv6Addr,
- ) -> Result {
- let response = client
- .query(domain.clone(), DNSClass::IN, RecordType::AAAA)
- .await
- .or_unexpected_while("querying A record")?;
-
- let records = response.answers();
-
- if records.len() != 1 {
- return Ok(false);
- }
-
- // if the single record isn't a AAAA, or it's not the target AAAA, then return false
- match records[0].data() {
- Some(RData::AAAA(remote_addr)) if remote_addr == addr => Ok(true),
- _ => return Ok(false),
- }
- }
-
async fn check_a(
client: &mut AsyncClient,
domain: &trust_dns_client::rr::Name,
@@ -62,15 +40,61 @@ impl DNSRecord {
let records = response.answers();
- if records.len() != 1 {
- return Ok(false);
+ for record in records {
+ if let Some(RData::A(record_addr)) = record.data() {
+ if record_addr == addr {
+ return Ok(true);
+ }
+ }
}
- // if the single record isn't a A, or it's not the target A, then return false
- match records[0].data() {
- Some(RData::A(remote_addr)) if remote_addr == addr => Ok(true),
- _ => return Ok(false),
+ return Ok(false);
+ }
+
+ async fn check_aaaa(
+ client: &mut AsyncClient,
+ domain: &trust_dns_client::rr::Name,
+ addr: &net::Ipv6Addr,
+ ) -> Result {
+ let response = client
+ .query(domain.clone(), DNSClass::IN, RecordType::AAAA)
+ .await
+ .or_unexpected_while("querying AAAA record")?;
+
+ let records = response.answers();
+
+ for record in records {
+ if let Some(RData::AAAA(record_addr)) = record.data() {
+ if record_addr == addr {
+ return Ok(true);
+ }
+ }
}
+
+ return Ok(false);
+ }
+
+ async fn check_cname(
+ client: &mut AsyncClient,
+ domain: &trust_dns_client::rr::Name,
+ cname: &trust_dns_client::rr::Name,
+ ) -> Result {
+ let response = client
+ .query(domain.clone(), DNSClass::IN, RecordType::CNAME)
+ .await
+ .or_unexpected_while("querying CNAME record")?;
+
+ let records = response.answers();
+
+ for record in records {
+ if let Some(RData::CNAME(record_cname)) = record.data() {
+ if record_cname == cname {
+ return Ok(true);
+ }
+ }
+ }
+
+ return Ok(false);
}
async fn check(
@@ -81,6 +105,7 @@ impl DNSRecord {
match self {
Self::A(addr) => Self::check_a(client, domain, &addr).await,
Self::AAAA(addr) => Self::check_aaaa(client, domain, &addr).await,
+ Self::CNAME(name) => Self::check_cname(client, domain, name.as_rr()).await,
}
}
}
diff --git a/src/domain/name.rs b/src/domain/name.rs
index ee462ca..4cbec1c 100644
--- a/src/domain/name.rs
+++ b/src/domain/name.rs
@@ -1,5 +1,5 @@
-use std::fmt;
use std::str::FromStr;
+use std::{cmp, fmt};
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
use trust_dns_client::rr as trust_dns_rr;
@@ -40,6 +40,12 @@ impl FromStr for Name {
}
}
+impl cmp::PartialEq for Name {
+ fn eq(&self, other: &Self) -> bool {
+ self.inner == other.inner
+ }
+}
+
impl Serialize for Name {
fn serialize(&self, serializer: S) -> Result
where
diff --git a/src/main.rs b/src/main.rs
index 1935039..409393b 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -34,14 +34,6 @@ struct Cli {
async fn main() {
let cli = Cli::parse();
- let config: domani::config::Config = {
- let path = &cli.config_path;
- let f = std::fs::File::open(path)
- .unwrap_or_else(|e| panic!("failed to open config file at {}: {e}", path.display()));
- serde_yaml::from_reader(f)
- .unwrap_or_else(|e| panic!("failed to parse config file at {}: {e}", path.display()))
- };
-
env_logger::Builder::new()
.filter_level(cli.log_level)
.format_timestamp(
@@ -50,14 +42,47 @@ async fn main() {
)
.init();
+ let config = {
+ let mut config: domani::config::Config = {
+ let path = &cli.config_path;
+ let f = std::fs::File::open(path).unwrap_or_else(|e| {
+ panic!("failed to open config file at {}: {e}", path.display())
+ });
+ serde_yaml::from_reader(f).unwrap_or_else(|e| {
+ panic!("failed to parse config file at {}: {e}", path.display())
+ })
+ };
+
+ // primary_cname is a CNAME record which points to the primary domain of the service. Since
+ // the primary domain _must_ point to the service (otherwise HTTPS wouldn't work) it's
+ // reasonable to assume that a CNAME on any domain would suffice to point that domain to
+ // the service.
+ let primary_cname = domani::service::ConfigDNSRecord::CNAME {
+ name: config.service.primary_domain.clone(),
+ };
+
+ let dns_records_have_primary_cname = config
+ .service
+ .dns_records
+ .iter()
+ .any(|r| r == &primary_cname);
+
+ if !dns_records_have_primary_cname {
+ log::info!(
+ "Adding 'CNAME {}' to service.dns_records",
+ &config.service.primary_domain
+ );
+ config.service.dns_records.push(primary_cname);
+ }
+
+ config
+ };
+
let origin_store = domani::origin::git::FSStore::new(&config.origin)
.expect("git origin store initialization failed");
let domain_checker = {
let dns_records = config.service.dns_records.clone();
- if dns_records.len() == 0 {
- panic!("service.dns_records must have at least one record defined")
- }
domani::domain::checker::DNSChecker::new(
&config.domain.dns,
diff --git a/src/service.rs b/src/service.rs
index bb3c5dd..984ac3b 100644
--- a/src/service.rs
+++ b/src/service.rs
@@ -9,11 +9,12 @@ fn default_primary_domain() -> domain::Name {
domain::Name::from_str("localhost").unwrap()
}
-#[derive(Serialize, Deserialize, Clone)]
+#[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 for domain::checker::DNSRecord {
@@ -21,6 +22,7 @@ impl From for domain::checker::DNSRecord {
match r {
ConfigDNSRecord::A { addr } => Self::A(addr),
ConfigDNSRecord::AAAA { addr } => Self::AAAA(addr),
+ ConfigDNSRecord::CNAME { name } => Self::CNAME(name),
}
}
}
diff --git a/src/service/http.rs b/src/service/http.rs
index ada0aed..f442f99 100644
--- a/src/service/http.rs
+++ b/src/service/http.rs
@@ -230,6 +230,9 @@ impl<'svc> Service {
flat_config: service::util::FlatConfig,
dns_records: &'a [service::ConfigDNSRecord],
challenge_token: String,
+
+ domain_is_zone_apex: bool,
+ dns_records_have_cname: bool,
}
let config: domain::Domain = match domain_config.try_into() {
@@ -249,6 +252,12 @@ impl<'svc> Service {
}
};
+ let domain_is_zone_apex = args.domain.as_rr().num_labels() == 2;
+ let dns_records_have_cname = self.config.dns_records.iter().any(|r| match r {
+ service::ConfigDNSRecord::CNAME { .. } => true,
+ _ => false,
+ });
+
self.render_page(
"/domain_init.html",
Response {
@@ -256,6 +265,9 @@ impl<'svc> Service {
flat_config: config.into(),
dns_records: &self.config.dns_records,
challenge_token: config_hash,
+
+ domain_is_zone_apex,
+ dns_records_have_cname,
},
)
}
diff --git a/src/service/http/tpl/domain_init.html b/src/service/http/tpl/domain_init.html
index 897d497..ca780c4 100644
--- a/src/service/http/tpl/domain_init.html
+++ b/src/service/http/tpl/domain_init.html
@@ -49,11 +49,21 @@ query for your domain name. It can be one or more of:
{{ this.type }} |
{{ lookup ../data "domain" }} |
+ {{ #if this.name }}
+ {{ this.name }} |
+ {{ else }}
{{ this.addr }} |
+ {{ /if }}
{{ /each }}
+{{ #if data.domain_is_zone_apex }}{{ #if data.dns_records_have_cname }}
+(Please note that not all DNS providers support putting a CNAME at the zone
+apex, while others support it via an alternative record type like ALIAS or
+ANAME.)
+{{ /if }}{{ /if }}
+
Once both entries are installed, you can hit the following button to check
your configuration and set up your domain.