wip: refactor

This commit is contained in:
Hannaeko 2025-03-25 23:20:09 +01:00
parent 419b78b55e
commit c1d09cd391
13 changed files with 165 additions and 231 deletions

View file

@ -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<Vec<record::Record>, DnsDriverError> {
async fn get_records(&self, zone: &str) -> Result<internal::RecordList, DnsDriverError> {
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<wire::ParseError> for DnsDriverError {
}
}
impl From<Error> for DnsDriverError {
fn from(value: Error) -> Self {
DnsDriverError::OperationError { reason: Box::new(value) }
}
}
/*
use trust_dns_proto::DnsHandle;
use trust_dns_client::client::ClientHandle;

View file

@ -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<dyn ZoneDriver>;
pub type BoxedRecordDriver = Arc<dyn RecordDriver>;
@ -25,8 +25,8 @@ pub trait ZoneDriver: Send + Sync {
#[async_trait]
pub trait RecordDriver: Send + Sync {
async fn get_records(&self, zone: &str) -> Result<Vec<record::Record>, DnsDriverError>;
async fn add_records(&self, zone: &str, new_records: &[record::DnsRecordImpl]) -> Result<(), DnsDriverError>;
async fn get_records(&self, zone: &str) -> Result<internal::RecordList, DnsDriverError>;
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<dns::Record>, new_records: Vec<dns::Record>) -> ConnectorResult<()>;
//async fn delete_records(&mut self, zone: dns::Name, class: dns::DNSClass, records: Vec<dns::Record>) -> ConnectorResult<()>;

View file

@ -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<DnsDriverError> for Error {
}
}
impl From<RecordParseError> for Error {
fn from(value: RecordParseError) -> Self {
impl From<RDataValidationError> 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<RecordValidationError> 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<RecordError > 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<TemplateError> for Error {
}
}
impl From<RecordError > 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<ProtoDnsError> for Error {
fn from(value: ProtoDnsError) -> Self {
match value {
@ -400,6 +397,12 @@ impl From<ProtoDnsError> for Error {
"rtype": rtype,
}))
},
ProtoDnsError::NameParseError { input } => {
Error::new("proto:dns:name_unknown", "Unknown error while parsing name")
.with_details(json!({
"input": input
}))
}
}
}
}

View file

@ -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<Name: ToName, Octs: AsRef<[u8]>> ComposeRecordData for ParsedRData<Name, Oc
}
}
}
/* --------- Records --------- */
pub(crate) type RecordImpl = Record<
Name<Vec<u8>>,
ParsedRData<Name<Vec<u8>>,Vec<u8>>
>;
impl<Name: ToString, Oct: AsRef<[u8]>> From<Record<Name, ParsedRData<Name, Oct>>> for internal::Record {
fn from(value: Record<Name, ParsedRData<Name, Oct>>) -> 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<internal::Record> for RecordImpl {
type Error = Error;
fn try_from(value: internal::Record) -> Result<Self, Self::Error> {
let owner = value.name.to_string();
let owner = owner.parse::<Name<_>>().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<RecordImpl>
}
impl TryFrom<internal::RecordList> for RecordList {
type Error = Error;
fn try_from(value: internal::RecordList) -> Result<Self, Self::Error> {
let mut records = Vec::with_capacity(value.records.len());
for record in value.records.into_iter() {
records.push(record.try_into()?)
}
Ok(RecordList { records })
}
}

View file

@ -1,2 +1,5 @@
pub mod rdata;
pub mod record;
pub use rdata::*;
pub use record::*;

View file

@ -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<internal::RData, Vec<Error>> {
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::<Ipv4Addr>().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::<Ipv6Addr>().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);

View file

@ -1,3 +1,7 @@
pub mod rdata;
pub mod record;
pub mod base;
pub use rdata::*;
pub use record::*;
pub use base::*;

View file

@ -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<u8>,
}

View file

@ -20,6 +20,4 @@ pub use zone::{Zone, AddZoneMemberRequest, CreateZoneRequest};
*/
pub mod zone;
//pub mod rdata;
pub mod record;
pub mod dns;

View file

@ -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<Error> },
}
pub(crate) type DnsRecordImpl = DnsRecord<
Name<Vec<u8>>,
proto::dns::ParsedRData<Name<Vec<u8>>,Vec<u8>>
>;
#[derive(Debug, Deserialize, Serialize)]
pub struct Record {
pub name: String,
pub ttl: u32,
#[serde(flatten)]
pub rdata: external::RData
}
// TODO: Proto
impl<Name: ToString, Oct: AsRef<[u8]>> From<DnsRecord<Name, proto::dns::ParsedRData<Name, Oct>>> for Record {
fn from(value: DnsRecord<Name, proto::dns::ParsedRData<Name, Oct>>) -> 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<Vec<u8>>) -> Result<DnsRecordImpl, Vec<Error>> {
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::<Name<_>>().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<Record>);
impl RecordList {
fn convert(self, zone_name: &Name<Vec<u8>>) -> Result<Vec<DnsRecordImpl>, Vec<Error>> {
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<DnsRecordImpl>
}
impl AddRecordsRequest {
pub fn validate(self, zone_name: &str) -> Result<AddRecords, Error> {
let zone_name: Name<Vec<u8>> = 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 }))
}
}
}

View file

@ -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)
}
}

View file

@ -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<String>,
State(app): State<AppState>,
) -> Result<Json<RecordList>, Error>
) -> Result<Json<external::RecordList>, 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<String>,
State(app): State<AppState>,
Json(add_records): Json<AddRecordsRequest>,
) -> Result<Json<Vec<Record>>, Error>
Json(add_records): Json<external::AddRecords>,
) -> Result<Json<external::RecordList>, 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()))
}
/*

View file

@ -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<String>,
@ -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),
})
))
}