diff --git a/Cargo.lock b/Cargo.lock index a958475..116e8eb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -68,12 +68,13 @@ checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "axum" -version = "0.8.0-rc.1" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b212eb691030da38f1eff381777e431eb6f0760a0d02ffcb1702f1da9894e2" +checksum = "6d6fd624c75e18b3b4c6b9caf42b1afe24437daaee904069137d8bab077be8b8" dependencies = [ "axum-core", "bytes", + "form_urlencoded", "futures-util", "http", "http-body", @@ -100,9 +101,9 @@ dependencies = [ [[package]] name = "axum-core" -version = "0.5.0-rc.1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30256e79153b08607dcf9e0a72bd31564bc9228b9f145d1e1a29e3d01ad1fd16" +checksum = "df1362f362fd16024ae199c1970ce98f9661bf5ef94b9808fee734bc3698b733" dependencies = [ "bytes", "futures-util", diff --git a/Cargo.toml b/Cargo.toml index c3efee2..68e7a7b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ name = "nomilo" version = "0.2.0-dev" authors = ["DNS Witch Collective "] -edition = "2021" +edition = "2024" license = "AGPL-3.0-or-later" readme = "README.md" repository = "https://git.familier.net.eu.org/dns-witch/nomilo" @@ -20,7 +20,7 @@ tokio = {version = "1", default-features = false, features = [ "macros", "rt-mul #rand = "0.8" tera = { version = "1", default-features = false } domain = { version = "0.10.3", features = [ "tsig", "unstable-client-transport" ]} -axum = { version = "0.8.0-rc.1", default-features = false, features = [ "http1", "json", "form", "query", "tokio" ]} +axum = { version = "0.8.1", default-features = false, features = [ "http1", "json", "form", "query", "tokio" ]} bb8 = { version = "0.9" } rusqlite = { version = "0.32"} async-trait = { version = "0.1" } diff --git a/assets/styles/main.css b/assets/styles/main.css index 2ed9d6a..5521aa9 100644 --- a/assets/styles/main.css +++ b/assets/styles/main.css @@ -23,6 +23,14 @@ main { margin: auto; } +h1 { + font-weight: 200; +} + +h2 { + font-weight: 300; +} + article.domain { margin-bottom: 2em; } @@ -38,7 +46,7 @@ article.domain header h3.folder-tab { display: flex; flex-direction: column; justify-content: center; - padding: 0 1em; + padding: 0 1rem; border-top-left-radius: .3rem; background-color: #f2e0fd; margin: 0; @@ -52,26 +60,23 @@ article.domain header h3.folder-tab ~ .sep { background-color: #f2e0fd; height: 100%; clip-path: url("#corner-folder-tab-right"); + flex-shrink: 0; +} + +article.domain .records { + background: #f2e0fd; + padding: 1rem; + border-radius: 0 .3rem .3rem .3rem; +} + +article.domain .records h4 { + margin: 0; } article.domain .records > ul { - background: #f2e0fd; margin: 0; padding: 0; list-style: none; - padding: 1rem;; - border-radius: 0 .3rem .3rem .3rem; - display: grid; - grid-template-columns: auto 1fr; - row-gap: 1rem; - column-gap: 0; -} - -article.domain .records .rrset { - display: grid; - align-items: baseline; - grid-template-columns: subgrid; - grid-column: 1 / 3; } article.domain .records .rrset .rtype { @@ -80,18 +85,8 @@ article.domain .records .rrset .rtype { gap: .5em; } -article.domain .records .rrset .rtype::after { - content: ''; - display: block; - flex: 1; - border-bottom: .2rem solid #850085; - padding-left: 1em; - position: relative; - bottom: .25rem; -} - article.domain .records .rrset ul { - padding: 0; + padding: 1rem 0 1rem 2rem; display: flex; flex-direction: column; gap: .5rem; @@ -105,32 +100,16 @@ article.domain .records .rrset li { gap: .5rem; } -article.domain .records .rrset li::before { - content: ''; - height: 1em; - width: 1rem; - border-bottom: .2rem solid #850085; - position: relative; - bottom: .25rem; -} - -article.domain .records .rrset li:not(:first-child)::before { - border-left: .2rem solid #850085; - border-bottom-left-radius: .3rem; -} - -article.domain .records .rrset li:not(:last-child)::after { - content: ''; - height: 100%; - width: 1rem; - position: absolute; - top: 1.1em; - border-left: .2rem solid #850085; +article.domain .records .rrset .rdata { + display: flex; + gap: .2rem; + flex-wrap: wrap; } article.domain .records .rrset .rdata-main { display: flex; gap: .3rem; + margin-right: .1rem; } article.domain .records .rrset .rdata-main .pill { @@ -138,7 +117,6 @@ article.domain .records .rrset .rdata-main .pill { } article.domain .records .rrset .rdata-complementary { - margin-top: .2em; font-size: .9em; gap: .2rem; display: flex; diff --git a/dev-scripts/zones/example.com.zone b/dev-scripts/zones/example.com.zone index 5de5bb0..f1a62de 100644 --- a/dev-scripts/zones/example.com.zone +++ b/dev-scripts/zones/example.com.zone @@ -6,6 +6,12 @@ example.com. IN SOA ns.example.com. admin.example.com. ( 300 ; minimum (5 minutes) ) +example.com. 600 IN A 198.51.100.3 +example.com. 600 IN AAAA 2001:db8:cafe:bc68::2 + +example.com. 3600 IN MX 1 srv1.example.com. +example.com. 3600 IN MX 100 mail.example.net. + example.com. 84600 IN NS ns.example.com. ns.example.com. 84600 IN A 198.51.100.3 diff --git a/src/main.rs b/src/main.rs index a272ffc..612de74 100644 --- a/src/main.rs +++ b/src/main.rs @@ -58,7 +58,8 @@ async fn main() { .route("/api/zones/{zone_name}/records", routing::get(routes::api::zones::get_zone_records)) .route("/api/zones/{zone_name}/records", routing::post(routes::api::zones::create_zone_records)) /* ----- UI ----- */ - .route("/zones/{zone_name}/records", routing::get(routes::ui::zones::get_zone_records_page)) + .route("/zones/{zone_name}/records", routing::get(routes::ui::zones::get_records_page)) + .route("/zones/{zone_name}/records/new", routing::get(routes::ui::zones::get_new_record_page)) .nest_service("/assets", ServeDir::new("assets")) .with_state(app_state); diff --git a/src/proto/dns.rs b/src/proto/dns.rs index 13b4037..8ac3c6b 100644 --- a/src/proto/dns.rs +++ b/src/proto/dns.rs @@ -461,22 +461,3 @@ impl TryFrom for RecordImpl { } } - - -pub struct RecordList { - pub records: Vec -} - -impl TryFrom for RecordList { - type Error = Error; - - fn try_from(value: internal::RecordList) -> Result { - let mut records = Vec::with_capacity(value.records.len()); - - for record in value.records.into_iter() { - records.push(record.try_into()?) - } - - Ok(RecordList { records }) - } -} diff --git a/src/resources/dns/friendly/mod.rs b/src/resources/dns/friendly/mod.rs index 5810030..3d9a147 100644 --- a/src/resources/dns/friendly/mod.rs +++ b/src/resources/dns/friendly/mod.rs @@ -1 +1,3 @@ pub mod rdata; + +pub use rdata::*; diff --git a/src/resources/dns/friendly/rdata.rs b/src/resources/dns/friendly/rdata.rs index 0a96a97..e016d2c 100644 --- a/src/resources/dns/friendly/rdata.rs +++ b/src/resources/dns/friendly/rdata.rs @@ -1,98 +1,309 @@ use std::fmt; -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Serialize, Serializer, ser::SerializeStruct}; -#[derive(Debug, Deserialize, Serialize)] -pub enum FriendlyRData { - Address(Address), - Service(Service), - MailServer(MailServer), // Include SPF, etc ?! - NameServer(NameServer), - TextData(TextData), - Alias(Alias), +use crate::resources::dns::internal; -} +#[derive(Debug, Deserialize, Serialize, PartialEq)] +#[serde(rename_all="lowercase")] pub enum FriendlyRType { Address, - Service, + Alias, MailServer, NameServer, + Service, + Spf, TextData, - Alias } -pub struct FriendlyRecord { +impl fmt::Display for FriendlyRType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + FriendlyRType::Address => write!(f, "address"), + FriendlyRType::Alias => write!(f, "alias"), + FriendlyRType::MailServer => write!(f, "mailserver"), + FriendlyRType::NameServer => write!(f, "nameserver"), + FriendlyRType::Service => write!(f, "service"), + FriendlyRType::Spf => write!(f, "spf"), + FriendlyRType::TextData => write!(f, "textdata"), + } + } +} + +pub trait HasRType { + fn rtype(&self) -> FriendlyRType; +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(tag = "_type")] +pub enum FriendlyRData { + Address(Address), + Alias(Alias), + MailServer(MailServer), + NameServer(NameServer), + Service(Service), + Spf(Spf), + TextData(TextData), +} + +impl HasRType for FriendlyRData { + fn rtype(&self) -> FriendlyRType { + match self { + FriendlyRData::Address(_) => FriendlyRType::Address, + FriendlyRData::Alias(_) => FriendlyRType::Alias, + FriendlyRData::MailServer(_) => FriendlyRType::MailServer, + FriendlyRData::NameServer(_) => FriendlyRType::NameServer, + FriendlyRData::Service(_) => FriendlyRType::Service, + FriendlyRData::Spf(_) => FriendlyRType::Spf, + FriendlyRData::TextData(_) => FriendlyRType::TextData, + } + } +} + +#[derive(Debug)] +pub struct FriendlyRecord { + ttl: i64, + data: T, +} + +impl Serialize for FriendlyRecord { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut state = serializer.serialize_struct("FriendlyRecord", 3)?; + state.serialize_field("ttl", &self.ttl)?; + state.serialize_field("data", &self.data)?; + state.serialize_field("rtype", &self.data.rtype())?; + state.end() + } +} + +impl FriendlyRecord { + fn new(ttl: u32, data: T) -> Self { + FriendlyRecord { + ttl: ttl.into(), + data, + } + } +} + +#[derive(Debug)] +pub struct FriendlyRecordSet { + ttl: i64, + data: Vec, +} + +impl Serialize for FriendlyRecordSet { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let mut state = serializer.serialize_struct("FriendlyRecordSet", 3)?; + state.serialize_field("ttl", &self.ttl)?; + state.serialize_field("data", &self.data)?; + state.serialize_field("rtype", &self.data.first().map(|d| d.rtype().to_string()))?; + state.end() + } +} + +impl FriendlyRecordSet { + fn new(ttl: u32) -> Self { + FriendlyRecordSet { + ttl: ttl.into(), + data: Vec::new() + } + } +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all="lowercase")] +pub enum ConfigurationType { + Mail, + Web, +} + +#[derive(Debug, Serialize)] +pub struct MailConfiguration { + pub servers: Option>, + pub spf: Option>, +} + +impl MailConfiguration { + pub fn new() -> Self { + MailConfiguration { + servers: None, + spf: None + } + } +} + +#[derive(Debug, Serialize)] +pub struct WebConfiguration { + pub addresses: Option>, +} + + +impl WebConfiguration { + pub fn new() -> Self { + WebConfiguration { + addresses: None, + } + } +} + +#[derive(Debug, Serialize)] +pub struct FriendlyRecordSetGroup { owner: String, - friendly_type: FriendlyRType, - rdata: FriendlyRData, - ttl: u32, + general_records: Vec>, + mail: Option, + web: Option, } -pub struct RecordSet { - friendly_type: FriendlyRType, - records: Vec +impl FriendlyRecordSetGroup { + pub fn new(owner: String) -> Self { + FriendlyRecordSetGroup { + owner, + general_records: Vec::new(), + mail: None, + web: None, + } + } } -pub struct RecordSetGroup { - owner: String, - rrsets: Vec +#[derive(Debug, Serialize)] +pub struct FriendlyRecords { + records: Vec, + aliases: Vec, } -pub struct FriendlyRecords(Vec); +impl FriendlyRecords { + pub fn new() -> Self { + FriendlyRecords { + records: Vec::new(), + aliases: Vec::new(), + } + } +} + +impl From for FriendlyRecords { + fn from(value: internal::RecordList) -> Self { + + let mut records = FriendlyRecords::new(); + + for record in value.records { + let internal::Record { name, ttl, rdata } = record; + let name = name.to_string(); + let rdata = rdata.friendly(&name, ttl); + + if rdata.is_none() { + continue; + } + + let rdata = rdata.unwrap(); + + if let FriendlyRData::Alias(alias) = rdata { + records.aliases.push(alias) + } else { + + if records.records.is_empty() { + records.records.push(FriendlyRecordSetGroup::new(name)); + } else { + let group = records.records.last().unwrap(); + + if group.owner != name { + records.records.push(FriendlyRecordSetGroup::new(name)); + } + } + + let current_group = records.records.last_mut().unwrap(); + + match rdata { + FriendlyRData::Address(address) => { + let web = current_group.web.get_or_insert_with(WebConfiguration::new); + let addresses = web.addresses.get_or_insert_with(|| FriendlyRecordSet::new(ttl)); + + addresses.data.push(address); + }, + FriendlyRData::MailServer(mailserver) => { + let mail: &mut MailConfiguration = current_group.mail.get_or_insert_with(MailConfiguration::new); + let servers = mail.servers.get_or_insert_with(|| FriendlyRecordSet::new(ttl)); + + servers.data.push(mailserver); + }, + FriendlyRData::Spf(spf) => { + let mail: &mut MailConfiguration = current_group.mail.get_or_insert_with(MailConfiguration::new); + + mail.spf = Some(FriendlyRecord::new(ttl, spf)); + }, + FriendlyRData::Alias(_) => {}, + data => { + // TODO: NS -> Skip if NS for zone (authority), create Delegation section with glue + DS for others (how to check if record is glue?) + + if current_group.general_records.is_empty() { + current_group.general_records.push(FriendlyRecordSet::new(ttl)); + } else { + let rrset = current_group.general_records.last().unwrap(); + if rrset.data.last().unwrap().rtype() != data.rtype() { + current_group.general_records.push(FriendlyRecordSet::new(ttl)); + } + } + + let current_rrset = current_group.general_records.last_mut().unwrap(); + current_rrset.data.push(data) + } + } + } + } + + records + } +} + +/* --------- RDATA --------- */ #[derive(Debug, Deserialize, Serialize)] pub struct Address { pub address: String } + +impl HasRType for Address { + fn rtype(&self) -> FriendlyRType { + FriendlyRType::Address + } +} + #[derive(Debug, Deserialize, Serialize)] pub struct Service { - pub service_type: String, - pub port: u16, - pub weight: u16, - pub priority: u16, + pub service_type: ServiceType, + pub service_name: Option, + pub service_protocol: Option, + pub port: i64, + pub weight: i64, + pub priority: i64, pub server: String, } +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "lowercase")] pub enum ServiceType { - ServiceProtocol { service_name: String, protocol: String }, - Service { service_name: String }, - None, + Other, } -impl ServiceType { - pub fn from_name(name: String) -> (Self, String) { - let labels: Vec<_> = name.splitn(3, '.').collect(); - let prefix: Vec<_> = labels.iter().cloned().take(2).map(|label| label.strip_prefix('_')).collect(); - - if prefix.is_empty() || prefix[0].is_none() { - (ServiceType::None, labels[0..].join(".").to_owned()) - } else if prefix.len() == 1 || prefix[1].is_none() { - (ServiceType::Service { service_name: prefix[0].unwrap().into() }, labels[1..].join(".").to_owned()) - } else { - (ServiceType::ServiceProtocol { service_name: prefix[0].unwrap().into(), protocol: prefix[1].unwrap().into() }, labels[2].to_owned()) - } - } -} - -impl fmt::Display for ServiceType { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - ServiceType::ServiceProtocol { service_name, protocol } => write!(f, "{}/{}", service_name, protocol), - ServiceType::Service { service_name, } => write!(f, "{}", service_name), - ServiceType::None => write!(f, "-"), - } - } -} - - #[derive(Debug, Deserialize, Serialize)] pub struct MailServer { - pub preference: u16, + pub preference: i64, pub mail_exchanger: String, } +impl HasRType for MailServer { + fn rtype(&self) -> FriendlyRType { + FriendlyRType::MailServer + } +} + #[derive(Debug, Deserialize, Serialize)] pub struct NameServer { pub target: String, @@ -105,5 +316,18 @@ pub struct TextData { #[derive(Debug, Deserialize, Serialize)] pub struct Alias { + pub from: String, pub target: String, + pub ttl: i64, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct Spf { + pub policy: String +} + +impl HasRType for Spf { + fn rtype(&self) -> FriendlyRType { + FriendlyRType::Spf + } } diff --git a/src/resources/dns/internal/rdata.rs b/src/resources/dns/internal/rdata.rs index add8c19..920c1bc 100644 --- a/src/resources/dns/internal/rdata.rs +++ b/src/resources/dns/internal/rdata.rs @@ -2,6 +2,8 @@ use std::net::{Ipv4Addr, Ipv6Addr}; use super::{Name, Rtype}; +use crate::resources::dns::friendly; + #[derive(Clone)] pub enum RData { A(A), @@ -29,6 +31,18 @@ impl RData { RData::Txt(_) => Rtype::Txt, } } + + pub fn friendly(self, owner: &str, ttl: u32) -> Option { + match self { + RData::A(data) => data.friendly(), + RData::Aaaa(data) => data.friendly(), + RData::Cname(data) => data.friendly(owner, ttl), + RData::Mx(data) => data.friendly(), + RData::Ns(data) => data.friendly(), + RData::Srv(data) => data.friendly(owner), + _ => None, + } + } } #[derive(Clone)] @@ -36,27 +50,70 @@ pub struct A { pub address: Ipv4Addr, } +impl A { + pub fn friendly(self) -> Option { + Some(friendly::FriendlyRData::Address(friendly::Address { + address: self.address.to_string() + })) + } +} + #[derive(Clone)] pub struct Aaaa { pub address: Ipv6Addr, } +impl Aaaa { + pub fn friendly(self) -> Option { + Some(friendly::FriendlyRData::Address(friendly::Address { + address: self.address.to_string() + })) + } +} + #[derive(Clone)] pub struct Cname { pub target: Name, } +impl Cname { + pub fn friendly(self, owner: &str, ttl: u32) -> Option { + Some(friendly::FriendlyRData::Alias(friendly::Alias { + from: owner.into(), + target: self.target.to_string(), + ttl: ttl.into(), + })) + } +} + #[derive(Clone)] pub struct Mx { pub preference: u16, pub mail_exchanger: Name, } +impl Mx { + pub fn friendly(self) -> Option { + Some(friendly::FriendlyRData::MailServer(friendly::MailServer { + preference: self.preference.into(), + mail_exchanger: self.mail_exchanger.to_string(), + })) + } +} + #[derive(Clone)] pub struct Ns { pub target: Name, } +impl Ns { + pub fn friendly(self) -> Option { + Some(friendly::FriendlyRData::NameServer(friendly::NameServer { + target: self.target.to_string(), + })) + } +} + #[derive(Clone)] pub struct Ptr { pub target: Name, @@ -81,6 +138,32 @@ pub struct Srv { pub weight: u16, } +impl Srv { + pub fn friendly(self, owner: &str) -> Option { + let labels: Vec<_> = owner.splitn(3, '.').collect(); + if labels.len() != 3 { + None + } else { + let service_name = labels[0]. strip_prefix('_'); + let protocol = labels[1]. strip_prefix('_'); + if let (Some(service_name), Some(protocol)) = (service_name, protocol) { + Some(friendly::FriendlyRData::Service(friendly::Service { + service_type: friendly::ServiceType::Other, + service_name: Some(service_name.into()), + service_protocol: Some(protocol.into()), + port: self.port.into(), + weight: self.weight.into(), + priority: self.priority.into(), + server: self.server.to_string(), + })) + } else { + None + } + } + + } +} + #[derive(Clone)] pub struct Txt { pub text: Vec, diff --git a/src/resources/dns/mod.rs b/src/resources/dns/mod.rs index 11b23ca..d1cc4dd 100644 --- a/src/resources/dns/mod.rs +++ b/src/resources/dns/mod.rs @@ -1,3 +1,3 @@ pub mod external; pub mod internal; -//pub mod friendly; +pub mod friendly; diff --git a/src/resources/zone.rs b/src/resources/zone.rs index 3552789..e719b31 100644 --- a/src/resources/zone.rs +++ b/src/resources/zone.rs @@ -41,9 +41,8 @@ impl Zone { db.create_zone(create_zone).await } - pub async fn get_records(zone_name: &str, db: BoxedDb, record_driver: BoxedRecordDriver) ->Result { - let zone = db.get_zone_by_name(zone_name).await?; - let mut records = record_driver.get_records(&zone.name).await?; + pub async fn get_records(&self, record_driver: BoxedRecordDriver) ->Result { + let mut records = record_driver.get_records(&self.name).await?; records.sort(); diff --git a/src/routes/api/zones.rs b/src/routes/api/zones.rs index 84c260c..b0a07e4 100644 --- a/src/routes/api/zones.rs +++ b/src/routes/api/zones.rs @@ -21,7 +21,8 @@ pub async fn get_zone_records( State(app): State, ) -> Result, Error> { - Zone::get_records(&zone_name, app.db, app.records) + let zone = app.db.get_zone_by_name(&zone_name).await?; + zone.get_records(app.records) .await .map(|records| Json(records.into())) } diff --git a/src/routes/ui/zones.rs b/src/routes/ui/zones.rs index 962d844..ba7be22 100644 --- a/src/routes/ui/zones.rs +++ b/src/routes/ui/zones.rs @@ -1,26 +1,75 @@ -use axum::extract::{Path, State}; +use axum::extract::Request; +use axum::extract::{Query, Path, State}; +use serde::Deserialize; use serde_json::{Value, json}; +use crate::validation; use crate::AppState; use crate::errors::Error; -use crate::resources::zone::Zone; use crate::template::Template; -use crate::resources::dns::external; +use crate::resources::dns::friendly; -pub async fn get_zone_records_page( +pub async fn get_records_page( Path(zone_name): Path, State(app): State, + request: Request, ) -> Result, Error> { - let records = Zone::get_records(&zone_name, app.db, app.records).await?; - - //records.0.sort_by_key(|record| (&record.name, record.rdata)); + let zone = app.db.get_zone_by_name(&zone_name).await?; + let records = zone.get_records(app.records).await?; + let records = friendly::FriendlyRecords::from(records.clone()); Ok(Template::new( "pages/records.html", app.template_engine, json!({ - "current_zone": zone_name, - "records": external::RecordList::from(records), + "current_zone": zone.name, + "records": records, + "url": request.uri().to_string(), + }) + )) +} + +#[derive(Deserialize)] +pub struct NewRecordQuery { + subdomain: Option, + config: Option, + rtype: Option, +} + +pub async fn get_new_record_page( + Path(zone_name): Path, + State(app): State, + Query(params): Query, + request: Request, +) -> Result, Error> { + let zone = app.db.get_zone_by_name(&zone_name).await?; + + // Syntax validation of the subdomain + let (name, domain_error) = if let Some(input_name) = params.subdomain { + if input_name.is_empty() { + (Some(zone.name.clone()), None) + } else { + let new_name = format!("{input_name}.{zone_name}"); + let res = validation::normalize_domain(&new_name); + match res { + Err(error) => (Some(input_name), Some(error)), + Ok(new_name) => (Some(new_name), None), + } + } + } else { + (None, None) + }; + + Ok(Template::new( + "pages/new_record.html", + app.template_engine, + json!({ + "current_zone": zone.name, + "new_record_name": name, + "domain_error": domain_error, + "config": params.config, + "rtype": params.rtype, + "url": request.uri().to_string(), }) )) } diff --git a/src/validation.rs b/src/validation.rs index 1f4ac14..3c361ba 100644 --- a/src/validation.rs +++ b/src/validation.rs @@ -13,11 +13,13 @@ pub enum DomainValidationError { /// https://doc.zonemaster.fr/v2024.1/specifications/tests/RequirementsAndNormalizationOfDomainNames.html /// TODO: No support of dots in labels, how to handle RNAME in SOA? pub fn normalize_domain(domain_name: &str) -> Result { - let domain = domain_name.strip_prefix('.').unwrap_or(domain_name).to_lowercase(); + if domain_name.is_empty() { + return Err(Error::from(DomainValidationError::EmptyDomain)) + } - if domain.is_empty() { - Err(Error::from(DomainValidationError::EmptyDomain)) - } else if domain.as_bytes().len() > 255 { + let domain = domain_name.strip_suffix('.').unwrap_or(domain_name).to_lowercase(); + + if domain.as_bytes().len() > 255 { Err(Error::from(DomainValidationError::DomainTooLong { length: domain.as_bytes().len() })) } else { let labels = domain.split('.').collect::>(); @@ -48,6 +50,7 @@ pub fn normalize_domain(domain_name: &str) -> Result { Ok(domain) } + } pub enum TxtParseError { MissingEscape,