From 76aa89412304e35e8957742bdeb2ab3dac9c3b0e Mon Sep 17 00:00:00 2001 From: Hannaeko Date: Tue, 25 Mar 2025 23:20:36 +0100 Subject: [PATCH] wip: add missing files --- src/resources/dns/external/record.rs | 119 +++++++++++++++++++++++++++ src/resources/dns/friendly/mod.rs | 1 + src/resources/dns/friendly/rdata.rs | 109 ++++++++++++++++++++++++ src/resources/dns/internal/base.rs | 90 ++++++++++++++++++++ src/resources/dns/internal/record.rs | 27 ++++++ 5 files changed, 346 insertions(+) create mode 100644 src/resources/dns/external/record.rs create mode 100644 src/resources/dns/friendly/mod.rs create mode 100644 src/resources/dns/friendly/rdata.rs create mode 100644 src/resources/dns/internal/base.rs create mode 100644 src/resources/dns/internal/record.rs diff --git a/src/resources/dns/external/record.rs b/src/resources/dns/external/record.rs new file mode 100644 index 0000000..905d96d --- /dev/null +++ b/src/resources/dns/external/record.rs @@ -0,0 +1,119 @@ +use serde::{Deserialize, Serialize}; + +use super::rdata::RData; +use crate::resources::dns::internal; +use crate::{errors::Error, validation}; +use crate::macros::{append_errors, push_error}; + +pub enum RecordError { + Validation { suberrors: Vec }, +} + +pub enum RecordValidationError { + NotInZone { name: String, zone: String }, +} + + +#[derive(Debug, Deserialize, Serialize)] +pub struct Record { + pub name: String, + pub ttl: u32, + #[serde(flatten)] + pub rdata: RData +} + +impl From for Record { + fn from(value: internal::Record) -> Self { + Record { + name: value.name.to_string(), + ttl: value.ttl, + rdata: value.rdata.into(), + } + } +} + +impl Record { + fn validate(self, zone_name: &internal::Name) -> Result> { + let mut errors = Vec::new(); + + let name = push_error!(validation::normalize_domain(&self.name), errors, "/name").map(internal::Name::new); + + let name = name.and_then(|name| { + if !name.ends_with(zone_name) { + errors.push( + Error::from(RecordValidationError::NotInZone { name: self.name, zone: zone_name.to_string() }) + .with_path("/name") + ); + None + } else { + Some(name) + } + }); + // TODO: validate ttl + let rdata = append_errors!(self.rdata.validate(), errors, "/rdata"); + + + if errors.is_empty() { + Ok(internal::Record { + name: name.unwrap(), + ttl: self.ttl, + rdata: rdata.unwrap(), + }) + } else { + Err(errors) + } + } +} + + +#[derive(Debug, Deserialize, Serialize)] +pub struct RecordList(pub Vec); + +impl From for RecordList { + fn from(value: internal::RecordList) -> Self { + let records = value.records.into_iter().map(Record::from).collect(); + RecordList(records) + } +} + +impl RecordList { + fn validate(self, zone_name: &internal::Name) -> Result> { + let mut errors = Vec::new(); + let mut records = Vec::new(); + + for (index, record) in self.0.into_iter().enumerate() { + let record = append_errors!(record.validate(zone_name), errors, &format!("/{index}")); + + if let Some(record) = record { + records.push(record) + } + } + + if errors.is_empty() { + Ok(internal::RecordList { records }) + } else { + Err(errors) + } + } +} + + +#[derive(Debug,Deserialize)] +pub struct AddRecords { + pub new_records: RecordList +} + +impl AddRecords { + pub fn validate(self, zone_name: &internal::Name) -> Result { + let mut errors = Vec::new(); + let records = append_errors!(self.new_records.validate(zone_name), errors, "/new_records"); + + if errors.is_empty() { + Ok(internal::AddRecords { + new_records: records.unwrap(), + }) + } else { + Err(Error::from(RecordError::Validation { suberrors: errors })) + } + } +} diff --git a/src/resources/dns/friendly/mod.rs b/src/resources/dns/friendly/mod.rs new file mode 100644 index 0000000..5810030 --- /dev/null +++ b/src/resources/dns/friendly/mod.rs @@ -0,0 +1 @@ +pub mod rdata; diff --git a/src/resources/dns/friendly/rdata.rs b/src/resources/dns/friendly/rdata.rs new file mode 100644 index 0000000..0a96a97 --- /dev/null +++ b/src/resources/dns/friendly/rdata.rs @@ -0,0 +1,109 @@ +use std::fmt; + +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Deserialize, Serialize)] +pub enum FriendlyRData { + Address(Address), + Service(Service), + MailServer(MailServer), // Include SPF, etc ?! + NameServer(NameServer), + TextData(TextData), + Alias(Alias), + +} + +pub enum FriendlyRType { + Address, + Service, + MailServer, + NameServer, + TextData, + Alias +} + +pub struct FriendlyRecord { + owner: String, + friendly_type: FriendlyRType, + rdata: FriendlyRData, + ttl: u32, +} + +pub struct RecordSet { + friendly_type: FriendlyRType, + records: Vec +} + +pub struct RecordSetGroup { + owner: String, + rrsets: Vec +} + +pub struct FriendlyRecords(Vec); + +#[derive(Debug, Deserialize, Serialize)] +pub struct Address { + pub address: String +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct Service { + pub service_type: String, + pub port: u16, + pub weight: u16, + pub priority: u16, + pub server: String, +} + +pub enum ServiceType { + ServiceProtocol { service_name: String, protocol: String }, + Service { service_name: String }, + None, +} + +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 mail_exchanger: String, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct NameServer { + pub target: String, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct TextData { + pub text: String, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct Alias { + pub target: String, +} diff --git a/src/resources/dns/internal/base.rs b/src/resources/dns/internal/base.rs new file mode 100644 index 0000000..494bf10 --- /dev/null +++ b/src/resources/dns/internal/base.rs @@ -0,0 +1,90 @@ +use std::fmt; + +#[derive(Clone, Eq, PartialEq)] +pub struct Name { + name: String +} + +impl Name { + pub fn new(name: String) -> Self { + Name { + name, + } + } + + pub fn ends_with(&self, other: &Name) -> bool { + self.name == other.name || self.name.ends_with(&(String::from(".") + &other.name)) + } +} + +impl fmt::Display for Name { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.name) + } +} + +impl Ord for Name { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + let mut labels = self.name.split('.').rev(); + let mut other_labels = other.name.split('.').rev(); + + loop { + match (labels.next(), other_labels.next()) { + (Some(label), Some(other_label)) => match label.cmp(other_label) { + std::cmp::Ordering::Equal => (), + res => return res, + }, + (None, Some(_)) => return std::cmp::Ordering::Less, + (Some(_), None) => return std::cmp::Ordering::Greater, + (None, None) => return std::cmp::Ordering::Equal, + } + } + } +} + +impl PartialOrd for Name { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +#[derive(Eq, PartialEq)] +pub enum Rtype { + A, + Aaaa, + Cname, + Mx, + Ns, + Ptr, + Soa, + Srv, + Txt +} + +impl Rtype { + pub fn value(&self) -> u16 { + match self { + Rtype::A => 1, + Rtype::Aaaa => 28, + Rtype::Cname => 5, + Rtype::Mx => 15, + Rtype::Ns => 2, + Rtype::Ptr => 12, + Rtype::Soa => 6, + Rtype::Srv => 33, + Rtype::Txt => 16, + } + } +} + +impl Ord for Rtype { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.value().cmp(&other.value()) + } +} + +impl PartialOrd for Rtype { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} diff --git a/src/resources/dns/internal/record.rs b/src/resources/dns/internal/record.rs new file mode 100644 index 0000000..2d5be24 --- /dev/null +++ b/src/resources/dns/internal/record.rs @@ -0,0 +1,27 @@ +use super::rdata::RData; +use super::Name; + +#[derive(Clone)] +pub struct Record { + pub name: Name, + pub ttl: u32, + pub rdata: RData +} + +#[derive(Clone)] +pub struct RecordList { + pub records: Vec, +} + +impl RecordList { + pub fn sort(&mut self) { + self.records.sort_by(|r1, r2| { + let key1 = (&r1.name, r1.rdata.rtype()); + let key2 = (&r2.name, r2.rdata.rtype()); + key1.cmp(&key2) + }); + } +} +pub struct AddRecords { + pub new_records: RecordList +}