From c1d09cd3918ea75252d6d70e3f5e495f2deb7ab8 Mon Sep 17 00:00:00 2001 From: Hannaeko Date: Tue, 25 Mar 2025 23:20:09 +0100 Subject: [PATCH] wip: refactor --- src/dns/dns_driver.rs | 18 ++-- src/dns/mod.rs | 6 +- src/errors.rs | 67 +++++++------- src/proto/dns.rs | 59 +++++++++++- src/resources/dns/external/mod.rs | 3 + src/resources/dns/external/rdata.rs | 26 ++---- src/resources/dns/internal/mod.rs | 4 + src/resources/dns/internal/rdata.rs | 39 ++++---- src/resources/mod.rs | 2 - src/resources/record/mod.rs | 135 ---------------------------- src/resources/zone.rs | 10 +-- src/routes/api/zones.rs | 23 ++--- src/routes/ui/zones.rs | 4 +- 13 files changed, 165 insertions(+), 231 deletions(-) delete mode 100644 src/resources/record/mod.rs diff --git a/src/dns/dns_driver.rs b/src/dns/dns_driver.rs index 402826d..f006109 100644 --- a/src/dns/dns_driver.rs +++ b/src/dns/dns_driver.rs @@ -11,8 +11,9 @@ use domain::tsig::{Algorithm, Key, KeyName}; use domain::net::client::request::{self, RequestMessage, RequestMessageMulti, SendRequest, SendRequestMulti}; use tokio::net::TcpStream; -use crate::resources::record; +use crate::resources::dns::internal; use crate::proto; +use crate::errors::Error; use super::{RecordDriver, ZoneDriver, DnsDriverError}; use async_trait::async_trait; @@ -127,7 +128,7 @@ impl ZoneDriver for DnsDriver { impl RecordDriver for DnsDriver { /// ------------- AXFR ------------- - async fn get_records(&self, zone: &str) -> Result, DnsDriverError> { + async fn get_records(&self, zone: &str) -> Result { let mut msg = MessageBuilder::new_vec(); msg.header_mut().set_ad(true); @@ -172,7 +173,7 @@ impl RecordDriver for DnsDriver { // AXFR response ends with SOA, we remove it so it is not doubled in the response. records.pop(); - Ok(records) + Ok(internal::RecordList { records }) } /// ------------- Dynamic Update - RFC 2136 ------------- @@ -221,7 +222,7 @@ impl RecordDriver for DnsDriver { /// zone rrset rr Add to an RRset - async fn add_records(&self, zone: &str, new_records: &[record::DnsRecordImpl]) -> Result<(), DnsDriverError> { + async fn add_records(&self, zone: &str, new_records: internal::RecordList) -> Result<(), DnsDriverError> { let mut msg = MessageBuilder::new_vec(); msg.header_mut().set_opcode(Opcode::UPDATE); @@ -233,7 +234,8 @@ impl RecordDriver for DnsDriver { let mut msg = msg.authority(); - for record in new_records { + for record in new_records.records { + let record = proto::dns::RecordImpl::try_from(record)?; msg.push(record)?; } @@ -294,6 +296,12 @@ impl From for DnsDriverError { } } +impl From for DnsDriverError { + fn from(value: Error) -> Self { + DnsDriverError::OperationError { reason: Box::new(value) } + } +} + /* use trust_dns_proto::DnsHandle; use trust_dns_client::client::ClientHandle; diff --git a/src/dns/mod.rs b/src/dns/mod.rs index 1db0d65..ff379e0 100644 --- a/src/dns/mod.rs +++ b/src/dns/mod.rs @@ -4,7 +4,7 @@ use std::sync::Arc; use async_trait::async_trait; -use crate::resources::record; +use crate::resources::dns::internal; pub type BoxedZoneDriver = Arc; pub type BoxedRecordDriver = Arc; @@ -25,8 +25,8 @@ pub trait ZoneDriver: Send + Sync { #[async_trait] pub trait RecordDriver: Send + Sync { - async fn get_records(&self, zone: &str) -> Result, DnsDriverError>; - async fn add_records(&self, zone: &str, new_records: &[record::DnsRecordImpl]) -> Result<(), DnsDriverError>; + async fn get_records(&self, zone: &str) -> Result; + async fn add_records(&self, zone: &str, new_records: internal::RecordList) -> Result<(), DnsDriverError>; //async fn update_records(&mut self, zone: dns::Name, class: dns::DNSClass, old_records: Vec, new_records: Vec) -> ConnectorResult<()>; //async fn delete_records(&mut self, zone: dns::Name, class: dns::DNSClass, records: Vec) -> ConnectorResult<()>; diff --git a/src/errors.rs b/src/errors.rs index d2b475f..c50b555 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -8,7 +8,8 @@ use serde::{Serialize, Serializer}; use serde_json::{Value, json}; use crate::dns::DnsDriverError; -use crate::resources::record::{RecordError, RecordParseError}; +use crate::resources::dns::external::rdata::RDataValidationError; +use crate::resources::dns::external::record::{RecordError, RecordValidationError}; use crate::resources::zone::ZoneError; use crate::validation::{DomainValidationError, TxtParseError}; use crate::template::TemplateError; @@ -157,6 +158,9 @@ impl fmt::Display for Error { } } + +impl std::error::Error for Error {} + impl IntoResponse for Error { fn into_response(self) -> Response { if let Some(status) = self.status { @@ -319,41 +323,46 @@ impl From for Error { } } -impl From for Error { - fn from(value: RecordParseError) -> Self { +impl From for Error { + fn from(value: RDataValidationError) -> Self { match value { - RecordParseError::Ip4Address { input } => { + RDataValidationError::Ip4Address { input } => { Error::new("record:parse:ip4", "The following IPv4 address {input} is invalid. IPv4 addresses should have four numbers, each between 0 and 255, separated by dots.") .with_details(json!({ "input": input })) }, - RecordParseError::Ip6Address { input } => { + RDataValidationError::Ip6Address { input } => { Error::new("record:parse:ip6", "The following IPv4 address {input} is invalid. IPv6 addresses should have eight groups of four hexadecimal digit separated by colons. Leftmost zeros in a group can be omitted, sequence of zeros can be shorted by a double colons.") .with_details(json!({ "input": input })) }, - RecordParseError::RDataUnknown { input, field, rtype } => { - Error::new("record:parse:rdata_unknown", "Unknown error while parsing record rdata field") - .with_details(json!({ - "input": input, - "field": field, - "rtype": rtype, - })) - }, - RecordParseError::NameUnknown { input } => { - Error::new("record:parse:name_unknown", "Unknown error while parsing record name") - .with_details(json!({ - "input": input - })) - }, - RecordParseError::NotInZone { name, zone } => { + } + } +} + +impl From for Error { + fn from(value: RecordValidationError) -> Self { + match value { + RecordValidationError::NotInZone { name, zone } => { Error::new("record:parse:not_in_zone", "The domain name {name} is not in the current zone ({zone})") .with_details(json!({ "name": name, "zone": zone })) + }, + } + } +} + +impl From for Error { + fn from(value: RecordError) -> Self { + match value { + RecordError::Validation { suberrors } => { + Error::new("record:validation", "Error while validating input records") + .with_suberrors(suberrors) + .with_status(StatusCode::BAD_REQUEST) } } } @@ -377,18 +386,6 @@ impl From for Error { } } -impl From for Error { - fn from(value: RecordError) -> Self { - match value { - RecordError::Validation { suberrors } => { - Error::new("record:validation", "Error while validating input records") - .with_suberrors(suberrors) - .with_status(StatusCode::BAD_REQUEST) - } - } - } -} - impl From for Error { fn from(value: ProtoDnsError) -> Self { match value { @@ -400,6 +397,12 @@ impl From for Error { "rtype": rtype, })) }, + ProtoDnsError::NameParseError { input } => { + Error::new("proto:dns:name_unknown", "Unknown error while parsing name") + .with_details(json!({ + "input": input + })) + } } } } diff --git a/src/proto/dns.rs b/src/proto/dns.rs index bc64b95..13b4037 100644 --- a/src/proto/dns.rs +++ b/src/proto/dns.rs @@ -2,15 +2,17 @@ use std::fmt::Write; use domain::base::rdata::ComposeRecordData; use domain::base::wire::{Composer, ParseError}; -use domain::base::{Name, ParseRecordData, ParsedName, RecordData, Rtype, ToName, Ttl}; +use domain::base::{iana::Class, Name, ParseRecordData, ParsedName, RecordData, Record, Rtype, ToName, Ttl}; use domain::rdata; use domain::dep::octseq::{Parser, Octets}; use crate::resources::dns::internal; use crate::errors::Error; +#[derive(Debug)] pub enum ProtoDnsError { RDataUnknown { input: String, field: String, rtype: String }, + NameParseError { input: String } } /* --------- A --------- */ @@ -423,3 +425,58 @@ impl> ComposeRecordData for ParsedRData>, + ParsedRData>,Vec> +>; + + +impl> From>> for internal::Record { + fn from(value: Record>) -> Self { + internal::Record { + name: internal::Name::new(value.owner().to_string()), + ttl: value.ttl().as_secs(), + rdata: internal::RData::from(value.into_data()), + } + } +} + +impl TryFrom for RecordImpl { + type Error = Error; + + fn try_from(value: internal::Record) -> Result { + let owner = value.name.to_string(); + let owner = owner.parse::>().map_err(|e| { + Error::from(ProtoDnsError::NameParseError { + input: owner + }).with_cause(&e.to_string()) + })?; + + let ttl = Ttl::from_secs(value.ttl); + let data = value.rdata.try_into()?; + Ok(Record::new(owner, Class::IN, ttl, data)) + + } +} + + +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/external/mod.rs b/src/resources/dns/external/mod.rs index f567899..55840a5 100644 --- a/src/resources/dns/external/mod.rs +++ b/src/resources/dns/external/mod.rs @@ -1,2 +1,5 @@ pub mod rdata; +pub mod record; + pub use rdata::*; +pub use record::*; diff --git a/src/resources/dns/external/rdata.rs b/src/resources/dns/external/rdata.rs index ec00eb1..2202364 100644 --- a/src/resources/dns/external/rdata.rs +++ b/src/resources/dns/external/rdata.rs @@ -1,16 +1,20 @@ use std::fmt::Write; use std::net::{Ipv4Addr, Ipv6Addr}; -use domain::base::{Rtype, scan::Symbol}; +use domain::base::scan::Symbol; use serde::{Deserialize, Serialize}; use crate::errors::Error; use crate::validation; use crate::macros::{append_errors, push_error}; -use crate::resources::record::RecordParseError; use crate::resources::dns::internal; +pub enum RDataValidationError { + Ip4Address { input: String }, + Ip6Address { input: String }, +} + /// Type used to serialize / deserialize resource records data to response / request /// #[derive(Debug, Deserialize, Serialize)] @@ -34,20 +38,6 @@ pub enum RData { } impl RData { - pub fn rtype(&self) -> Rtype { - match self { - RData::A(_) => Rtype::A, - RData::Aaaa(_) => Rtype::AAAA, - RData::Cname(_) => Rtype::CNAME, - RData::Mx(_) => Rtype::MX, - RData::Ns(_) => Rtype::NS, - RData::Ptr(_) => Rtype::PTR, - RData::Soa(_) => Rtype::SOA, - RData::Srv(_) => Rtype::SRV, - RData::Txt(_) => Rtype::TXT, - } - } - pub fn validate(self) -> Result> { let rdata = match self { RData::A(data) => internal::RData::A(data.validate()?), @@ -101,7 +91,7 @@ impl A { let mut errors = Vec::new(); let address = push_error!(self.address.parse::().map_err(|e| { - Error::from(RecordParseError::Ip4Address { input: self.address }) + Error::from(RDataValidationError::Ip4Address { input: self.address }) .with_cause(&e.to_string()) .with_path("/address") }), errors); @@ -137,7 +127,7 @@ impl Aaaa { // TODO: replace with custom validation let address = push_error!(self.address.parse::().map_err(|e| { - Error::from(RecordParseError::Ip6Address { input: self.address }) + Error::from(RDataValidationError::Ip6Address { input: self.address }) .with_cause(&e.to_string()) .with_path("/address") }), errors); diff --git a/src/resources/dns/internal/mod.rs b/src/resources/dns/internal/mod.rs index 3d9a147..5e3d450 100644 --- a/src/resources/dns/internal/mod.rs +++ b/src/resources/dns/internal/mod.rs @@ -1,3 +1,7 @@ pub mod rdata; +pub mod record; +pub mod base; pub use rdata::*; +pub use record::*; +pub use base::*; diff --git a/src/resources/dns/internal/rdata.rs b/src/resources/dns/internal/rdata.rs index 552575e..add8c19 100644 --- a/src/resources/dns/internal/rdata.rs +++ b/src/resources/dns/internal/rdata.rs @@ -1,6 +1,8 @@ use std::net::{Ipv4Addr, Ipv6Addr}; -use std::fmt; +use super::{Name, Rtype}; + +#[derive(Clone)] pub enum RData { A(A), Aaaa(Aaaa), @@ -13,49 +15,54 @@ pub enum RData { Txt(Txt), } -pub struct Name { - name: String -} - -impl Name { - pub fn new(name: String) -> Self { - Name { - name, +impl RData { + pub fn rtype(&self) -> Rtype { + match self { + RData::A(_) => Rtype::A, + RData::Aaaa(_) => Rtype::Aaaa, + RData::Cname(_) => Rtype::Cname, + RData::Mx(_) => Rtype::Mx, + RData::Ns(_) => Rtype::Ns, + RData::Ptr(_) => Rtype::Ptr, + RData::Soa(_) => Rtype::Soa, + RData::Srv(_) => Rtype::Srv, + RData::Txt(_) => Rtype::Txt, } } } -impl fmt::Display for Name { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.name) - } -} - +#[derive(Clone)] pub struct A { pub address: Ipv4Addr, } +#[derive(Clone)] pub struct Aaaa { pub address: Ipv6Addr, } +#[derive(Clone)] pub struct Cname { pub target: Name, } +#[derive(Clone)] pub struct Mx { pub preference: u16, pub mail_exchanger: Name, } +#[derive(Clone)] pub struct Ns { pub target: Name, } +#[derive(Clone)] pub struct Ptr { pub target: Name, } +#[derive(Clone)] pub struct Soa { pub primary_server: Name, pub maintainer: Name, @@ -66,6 +73,7 @@ pub struct Soa { pub serial: u32, } +#[derive(Clone)] pub struct Srv { pub server: Name, pub port: u16, @@ -73,6 +81,7 @@ pub struct Srv { pub weight: u16, } +#[derive(Clone)] pub struct Txt { pub text: Vec, } diff --git a/src/resources/mod.rs b/src/resources/mod.rs index fb7d88e..9cc3a32 100644 --- a/src/resources/mod.rs +++ b/src/resources/mod.rs @@ -20,6 +20,4 @@ pub use zone::{Zone, AddZoneMemberRequest, CreateZoneRequest}; */ pub mod zone; -//pub mod rdata; -pub mod record; pub mod dns; diff --git a/src/resources/record/mod.rs b/src/resources/record/mod.rs deleted file mode 100644 index 9b29821..0000000 --- a/src/resources/record/mod.rs +++ /dev/null @@ -1,135 +0,0 @@ -use serde::{Deserialize, Serialize}; - -use domain::base::{iana::Class, Name, Record as DnsRecord, Ttl}; - -use crate::{errors::Error, validation}; -use crate::macros::{append_errors, push_error}; - -use crate::resources::dns::external; -use crate::resources::dns::internal; -use crate::proto; - -pub enum RecordParseError { - Ip4Address { input: String }, - Ip6Address { input: String }, - RDataUnknown { input: String, field: String, rtype: String }, - NameUnknown { input: String }, - NotInZone { name: String, zone: String }, -} - -pub enum RecordError { - Validation { suberrors: Vec }, -} - -pub(crate) type DnsRecordImpl = DnsRecord< - Name>, - proto::dns::ParsedRData>,Vec> ->; - -#[derive(Debug, Deserialize, Serialize)] -pub struct Record { - pub name: String, - pub ttl: u32, - #[serde(flatten)] - pub rdata: external::RData -} - -// TODO: Proto -impl> From>> for Record { - fn from(value: DnsRecord>) -> Self { - Record { - name: value.owner().to_string(), - ttl: value.ttl().as_secs(), - rdata: internal::RData::from(value.into_data()).into(), - } - } -} - -impl Record { - fn convert(self, zone_name: &Name>) -> Result> { - let mut errors = Vec::new(); - - let name = push_error!(validation::normalize_domain(&self.name), errors, "/name"); - - let name = name.and_then(|name| push_error!(name.parse::>().map_err(|e| { - Error::from(RecordParseError::NameUnknown { - input: self.name.clone() - }).with_cause(&e.to_string()) - }), errors, "/name")); - - let name = name.and_then(|name| { - if !name.ends_with(zone_name) { - errors.push( - Error::from(RecordParseError::NotInZone { name: self.name, zone: zone_name.to_string() }) - .with_path("/name") - ); - None - } else { - Some(name) - } - }); - - let ttl = Ttl::from_secs(self.ttl); - let rdata = append_errors!(self.rdata.validate(), errors, "/rdata"); - - - if errors.is_empty() { - // TODO: Split this in proto / external - let rdata = proto::dns::ParsedRData::try_from(rdata.unwrap()).unwrap(); - Ok(DnsRecord::new(name.unwrap(), Class::IN, ttl, rdata)) - } else { - Err(errors) - } - } -} - - -#[derive(Debug, Deserialize, Serialize)] -pub struct RecordList(pub Vec); - -impl RecordList { - fn convert(self, zone_name: &Name>) -> Result, Vec> { - let mut errors = Vec::new(); - let mut records = Vec::new(); - - for (index, record) in self.0.into_iter().enumerate() { - let record = append_errors!(record.convert(zone_name), errors, &format!("/{index}")); - - if let Some(record) = record { - records.push(record) - } - } - - if errors.is_empty() { - Ok(records) - } else { - Err(errors) - } - } -} - -#[derive(Debug,Deserialize)] -pub struct AddRecordsRequest { - pub new_records: RecordList -} - -pub struct AddRecords { - pub new_records: Vec -} - -impl AddRecordsRequest { - pub fn validate(self, zone_name: &str) -> Result { - let zone_name: Name> = zone_name.parse().expect("zone name is assumed to be valid"); - - let mut errors = Vec::new(); - let records = append_errors!(self.new_records.convert(&zone_name), errors, "/new_records"); - - if errors.is_empty() { - Ok(AddRecords { - new_records: records.unwrap(), - }) - } else { - Err(Error::from(RecordError::Validation { suberrors: errors })) - } - } -} diff --git a/src/resources/zone.rs b/src/resources/zone.rs index ced2977..3552789 100644 --- a/src/resources/zone.rs +++ b/src/resources/zone.rs @@ -7,7 +7,7 @@ use crate::database::{BoxedDb, sqlite::SqliteDB}; use crate::dns::{BoxedZoneDriver, BoxedRecordDriver, DnsDriverError}; use crate::errors::Error; use crate::macros::push_error; -use crate::resources::record::RecordList; +use crate::resources::dns::internal::RecordList; use crate::validation; pub enum ZoneError { @@ -45,13 +45,9 @@ impl Zone { let zone = db.get_zone_by_name(zone_name).await?; let mut records = record_driver.get_records(&zone.name).await?; - records.sort_by(|r1, r2| { - let key1 = (&r1.name, r1.rdata.rtype()); - let key2 = (&r2.name, r2.rdata.rtype()); - key1.cmp(&key2) - }); + records.sort(); - Ok(RecordList(records)) + Ok(records) } } diff --git a/src/routes/api/zones.rs b/src/routes/api/zones.rs index 5dc0739..84c260c 100644 --- a/src/routes/api/zones.rs +++ b/src/routes/api/zones.rs @@ -4,7 +4,8 @@ use axum::Json; use crate::AppState; use crate::errors::Error; use crate::resources::zone::{CreateZoneRequest, Zone}; -use crate::resources::record::{AddRecordsRequest, Record, RecordList}; +use crate::resources::dns::external; +use crate::resources::dns::internal; pub async fn create_zone( @@ -18,26 +19,26 @@ pub async fn create_zone( pub async fn get_zone_records( Path(zone_name): Path, State(app): State, -) -> Result, Error> +) -> Result, Error> { - Zone::get_records(&zone_name, app.db, app.records).await.map(Json) + Zone::get_records(&zone_name, app.db, app.records) + .await + .map(|records| Json(records.into())) } pub async fn create_zone_records( Path(zone_name): Path, State(app): State, - Json(add_records): Json, -) -> Result>, Error> + Json(add_records): Json, +) -> Result, Error> { let zone = app.db.get_zone_by_name(&zone_name).await?; - let add_records = add_records.validate(&zone.name)?; - app.records.add_records(&zone.name, &add_records.new_records).await?; - let records = add_records.new_records.into_iter() - .map(|r| r.into()) - .collect(); + let name = internal::Name::new(zone.name.clone()); + let add_records = add_records.validate(&name)?; + app.records.add_records(&zone.name, add_records.new_records.clone()).await?; - Ok(Json(records)) + Ok(Json(add_records.new_records.into())) } /* diff --git a/src/routes/ui/zones.rs b/src/routes/ui/zones.rs index 6f8dd0a..962d844 100644 --- a/src/routes/ui/zones.rs +++ b/src/routes/ui/zones.rs @@ -5,7 +5,7 @@ use crate::AppState; use crate::errors::Error; use crate::resources::zone::Zone; use crate::template::Template; - +use crate::resources::dns::external; pub async fn get_zone_records_page( Path(zone_name): Path, @@ -20,7 +20,7 @@ pub async fn get_zone_records_page( app.template_engine, json!({ "current_zone": zone_name, - "records": records, + "records": external::RecordList::from(records), }) )) }