diff --git a/Cargo.lock b/Cargo.lock index c3a9a05..fa2cd32 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -775,6 +775,7 @@ dependencies = [ "rusqlite", "serde", "serde_json", + "serde_urlencoded", "tera", "tokio", "tower", diff --git a/Cargo.toml b/Cargo.toml index 7a4e1a6..4517fca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,3 +28,4 @@ tower-http = { version = "0.6", default-features = false, features = [ "fs" ]} fluent-bundle = "0.15.3" unic-langid = "*" tower = "*" +serde_urlencoded = "*" diff --git a/locales/en/main.ftl b/locales/en/main.ftl index 7d88824..61d1f6f 100644 --- a/locales/en/main.ftl +++ b/locales/en/main.ftl @@ -29,3 +29,11 @@ zone-content-record-type-service = zone-content-new-record-button = New record ## Create record + +record-input-addresses = + .input-label = IP address #{ $index } + .error-record-parse-ip = Unexpected IP address format. The IP address + should be either an IPv4 address, like 198.51.100.3, or an IPv6 + address, like 2001:db8:cafe:bc68::2. + +button-save-configuration = Save configuration diff --git a/locales/fr/main.ftl b/locales/fr/main.ftl index ea94b46..d1147f2 100644 --- a/locales/fr/main.ftl +++ b/locales/fr/main.ftl @@ -29,3 +29,11 @@ zone-content-record-type-service = zone-content-new-record-button = Nouvel enregistrement ## Create record + +record-input-addresses = + .input-label = Adresse IP #{ $index } + .error-record-parse-ip = Format d'adresse IP inconnu. L'adresse IP doit ĂȘtre + soit une adresse IPv4, comme 198.51.100.3, soit une adresse IPv6, + comme 2001:db8:cafe:bc68::2. + +button-save-configuration = Sauvegarder la configuration diff --git a/src/errors.rs b/src/errors.rs index 270636c..49a3bd4 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -1,4 +1,5 @@ use std::fmt; +use std::collections::HashMap; use axum::http::{self, StatusCode}; use axum::response::{AppendHeaders, IntoResponse, Response}; @@ -10,6 +11,7 @@ use serde_json::{Value, json}; use crate::dns::DnsDriverError; use crate::resources::dns::external::rdata::RDataValidationError; use crate::resources::dns::external::record::{RecordError, RecordValidationError}; +use crate::resources::dns::friendly::InputDataError; use crate::resources::zone::ZoneError; use crate::validation::{DomainValidationError, TxtParseError}; use crate::template::TemplateError; @@ -41,6 +43,13 @@ pub fn serialize_status(status: &Option, serializer: S) -> Result } } +pub fn error_map(errors: Vec) -> HashMap { + errors.into_iter() + .filter_map(|mut error| + error.path.take().map(|path| (path, error)) + ).collect() +} + impl Error { pub fn new(code: &str, description: &str) -> Self { Error { @@ -338,6 +347,19 @@ impl From for Error { "input": input })) }, + RDataValidationError::IpAddress { input } => { + Error::new("record:parse:ip", "The IP address {input} is invalid. It should be either an IPv4 address, four numbers between 0 and 255 separated by dots, or and IPv6 address, eight groups of four hexadecimal digit separated by colons.") + .with_details(json!({ + "input": input + })) + }, + RDataValidationError::Number { min, max } => { + Error::new("record:parse:number", "Expected a number between {min} and {max}") + .with_details(json!({ + "min": min, + "max": max, + })) + } } } } @@ -414,3 +436,20 @@ impl From for Error { } } } + +impl From for Error { + fn from(value: InputDataError) -> Self { + match value { + InputDataError::TypeError { expected, found } => { + Error::new("input:type_error", "Expected to find type {expected} found type {found}.") + .with_details(json!({ + "expected": expected, + "found": found, + })) + }, + InputDataError::MissingValue => { + Error::new("input:missing_value", "The value at the given path is required.") + } + } + } +} diff --git a/src/form.rs b/src/form.rs new file mode 100644 index 0000000..20fc57c --- /dev/null +++ b/src/form.rs @@ -0,0 +1,250 @@ +use std::collections::HashMap; + +use axum::extract::{Request, FromRequest}; +use axum::response::{Response, IntoResponse}; +use axum::http::StatusCode; +use axum::Form; + +impl FromRequest for Node +where + S: Send + Sync, +{ + type Rejection = Response; + + async fn from_request(req: Request, state: &S) -> Result { + let Form(data): Form> = Form::from_request(req, state) + .await + .map_err(IntoResponse::into_response)?; + + let node = Node::from_key_value(data) + .map_err(|_| StatusCode::UNPROCESSABLE_ENTITY.into_response())?; + + Ok(node) + } +} + + +#[derive(Debug)] +pub enum FormError { + MismatchedType +} + +#[derive(Debug, PartialEq, Eq)] +pub enum Node { + Value(String), + Map(HashMap), + Sequence(Sequence) +} + +impl Node { + pub fn from_key_value(data: Vec<(String, String)>) -> Result { + let mut form = Node::Map(HashMap::new()); + for (key, value) in data { + + // Consider empty value not filled and remove them + if value.is_empty() { + continue; + } + + let path = Self::parse_key(&key); + let mut parent_node = &mut form; + + for window in path.windows(2) { + let parent_key = &window[0]; + let child_key = &window[1]; + + parent_node = match (parent_node, parent_key) { + (&mut Node::Map(ref mut map), Key::Attribute(key)) => { + map.entry(key.clone()).or_insert_with(|| child_key.new_node()) + }, + (&mut Node::Sequence(Sequence::ImplicitIndex(ref mut list)), Key::ImplicitIndex) => { + list.push(child_key.new_node()); + list.last_mut().unwrap() + }, + (&mut Node::Sequence(Sequence::ExplicitIndex(ref mut list)), Key::ExplicitIndex(index)) => { + list.entry(*index).or_insert_with(|| child_key.new_node()) + }, + _ => { + return Err(FormError::MismatchedType); + } + }; + } + + let last_key = path.last().unwrap(); + match (parent_node, last_key) { + (&mut Node::Map(ref mut map), Key::Attribute(key)) => { + map.insert(key.clone(), Node::Value(value)); + }, + (&mut Node::Sequence(Sequence::ImplicitIndex(ref mut list)), Key::ImplicitIndex) => { + list.push(Node::Value(value)) + }, + (&mut Node::Sequence(Sequence::ExplicitIndex(ref mut list)), Key::ExplicitIndex(index)) => { + list.insert(*index, Node::Value(value)); + }, + _ => { + return Err(FormError::MismatchedType); + } + } + } + + Ok(form) + + } + + pub fn parse_key(key: &str) -> Vec { + let keys = if let Some((head, tail)) = key.split_once('[') { + let mut keys = vec![head]; + keys.extend(tail.trim_end_matches(']').split("][")); + keys + } else { + vec![key] + }; + + keys.iter().map(|key| { + if key.is_empty() { + Key::ImplicitIndex + } else if let Ok(index) = key.parse::() { + Key::ExplicitIndex(index) + } else { + Key::Attribute(key.to_string()) + } + }).collect() + } + + pub fn to_json_value(self) -> serde_json::Value { + match self { + Node::Value(value) => serde_json::Value::String(value), + Node::Map(map) => { + let map = map.into_iter() + .map(|(key, node)| (key, node.to_json_value())) + .collect(); + serde_json::Value::Object(map) + }, + Node::Sequence(list) => { + let array = list.to_vec() + .into_iter() + .map(|node| node.to_json_value()) + .collect(); + + serde_json::Value::Array(array) + } + } + } +} + +#[derive(Debug, PartialEq, Eq)] +pub enum Sequence { + ImplicitIndex(Vec), + ExplicitIndex(HashMap), +} + +impl Sequence { + pub fn to_vec(self) -> Vec { + match self { + Sequence::ImplicitIndex(list) => list, + Sequence::ExplicitIndex(map) => { + let mut key_values: Vec<(usize, Node)> = map.into_iter().collect(); + key_values.sort_by_key(|(k, _)| *k); + key_values.into_iter().map(|(_, v)| v).collect() + } + } + } +} + +#[derive(Debug, PartialEq, Eq)] +pub enum Key { + ExplicitIndex(usize), + ImplicitIndex, + Attribute(String), +} + +impl Key { + pub fn new_node(&self) -> Node { + match self { + Key::ExplicitIndex(_) => Node::Sequence(Sequence::ExplicitIndex(HashMap::new())), + Key::ImplicitIndex => Node::Sequence(Sequence::ImplicitIndex(Vec::new())), + Key::Attribute(_) => Node::Map(HashMap::new()), + } + } +} + + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + #[test] + pub fn test_parse_key() { + let key = "records[0][addresses][][address]".to_string(); + let parsed_key = vec![ + Key::Attribute("records".to_string()), + Key::ExplicitIndex(0), + Key::Attribute("addresses".to_string()), + Key::ImplicitIndex, + Key::Attribute("address".to_string()), + ]; + assert_eq!(Node::parse_key(&key), parsed_key); + } + + #[test] + pub fn test_parse_key_value() { + let form_data = vec![ + ("records[0][addresses][][address]".to_string(), "123".to_string()), + ("records[0][addresses][][address]".to_string(), "abc".to_string()), + ]; + let mut address1 = HashMap::new(); + address1.insert("address".to_string(), Node::Value("123".to_string())); + let mut address2 = HashMap::new(); + address2.insert("address".to_string(), Node::Value("abc".to_string())); + + let addresses = vec![Node::Map(address1), Node::Map(address2)]; + + let mut record = HashMap::new(); + record.insert("addresses".to_string(), Node::Sequence(Sequence::ImplicitIndex(addresses))); + + let mut record_list = HashMap::new(); + record_list.insert(0, Node::Map(record)); + + let mut form = HashMap::new(); + form.insert("records".to_string(), Node::Sequence(Sequence::ExplicitIndex(record_list))); + + let parsed_form = Node::Map(form); + + assert_eq!(Node::from_key_value(form_data).unwrap(), parsed_form); + } + + #[test] + pub fn test_json_value() { + let mut address1 = HashMap::new(); + address1.insert("address".to_string(), Node::Value("123".to_string())); + let mut address2 = HashMap::new(); + address2.insert("address".to_string(), Node::Value("abc".to_string())); + + let addresses = vec![Node::Map(address1), Node::Map(address2)]; + + let mut record = HashMap::new(); + record.insert("addresses".to_string(), Node::Sequence(Sequence::ImplicitIndex(addresses))); + + let mut record_list = HashMap::new(); + record_list.insert(0, Node::Map(record)); + + let mut form = HashMap::new(); + form.insert("records".to_string(), Node::Sequence(Sequence::ExplicitIndex(record_list))); + + let parsed_form = Node::Map(form); + + let json_value = json!({ + "records": [ + { + "addresses": [ + { "address": "123" }, + { "address": "abc" }, + ] + } + ] + }); + + assert_eq!(parsed_form.to_json_value(), json_value); + } +} diff --git a/src/localization.rs b/src/localization.rs index d74e7d6..a30a1df 100644 --- a/src/localization.rs +++ b/src/localization.rs @@ -110,6 +110,10 @@ impl tera::Function for Localization { Ok(tera::Value::from(localized_message)) } + + fn is_safe(&self) -> bool { + true + } } fn fluent_value_from_tera<'s>(value: &'s tera::Value, name: &str) -> tera::Result> { diff --git a/src/macros.rs b/src/macros.rs index 5009b76..6646874 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -30,6 +30,65 @@ macro_rules! append_errors { }; } +macro_rules! check_type { + ($value:expr, Number, $errors:expr) => { + if let serde_json::Value::Number(value) = $value { + Some(value) + } else { + + let value = if let serde_json::Value::String(ref value) = $value { + value.parse::().ok() + .map(serde_json::Number::from) + } else { + None + }; + + if value.is_some() { + value + } else { + use crate::resources::dns::friendly::InputDataError; + use crate::resources::dns::friendly::ValueType; + use crate::errors::Error; + + $errors.push(Error::from(InputDataError::TypeError { expected: ValueType::Number, found: ValueType::from_value(&$value) })); + None + } + } + }; + ($value:expr, $type:ident, $errors:expr) => { + if let serde_json::Value::$type(value) = $value { + Some(value) + } else { + use crate::resources::dns::friendly::InputDataError; + use crate::resources::dns::friendly::ValueType; + use crate::errors::Error; + + $errors.push(Error::from(InputDataError::TypeError { expected: ValueType::$type, found: ValueType::from_value(&$value) })); + None + } + }; +} + +macro_rules! get_object_value { + ($value:expr, $key:expr, $errors:expr) => { + if let Some(value) = $value.remove($key) { + Some(value) + } else { + use crate::resources::dns::friendly::InputDataError; + use crate::errors::Error; + + $errors.push( + Error::from(InputDataError::MissingValue) + .with_path(concat!("/", $key)) + ); + None + } + }; +} + + pub(crate) use append_errors; pub(crate) use push_error; +pub(crate) use check_type; +pub(crate) use get_object_value; diff --git a/src/main.rs b/src/main.rs index 4af4872..9ba7f82 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,6 +8,7 @@ mod macros; mod template; mod proto; mod localization; +mod form; use std::sync::Arc; @@ -66,6 +67,7 @@ async fn main() { /* ----- UI ----- */ .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)) + .route("/zones/{zone_name}/records/new", routing::post(routes::ui::zones::post_new_record)) .nest_service("/assets", ServeDir::new("assets")) .with_state(app_state) .layer(localization.language_middleware("en".parse().unwrap())); diff --git a/src/resources/dns/external/rdata.rs b/src/resources/dns/external/rdata.rs index bec92d1..b42df36 100644 --- a/src/resources/dns/external/rdata.rs +++ b/src/resources/dns/external/rdata.rs @@ -13,6 +13,8 @@ use crate::resources::dns::internal; pub enum RDataValidationError { Ip4Address { input: String }, Ip6Address { input: String }, + IpAddress { input: String }, + Number { min: i128, max: i128 } } /// Type used to serialize / deserialize resource records data to response / request diff --git a/src/resources/dns/friendly/base.rs b/src/resources/dns/friendly/base.rs new file mode 100644 index 0000000..f12c80a --- /dev/null +++ b/src/resources/dns/friendly/base.rs @@ -0,0 +1,207 @@ +use std::net::IpAddr; + +use serde::Serialize; +use serde_json::Value; + +use crate::errors::Error; +use crate::macros::{push_error, append_errors, check_type}; +use crate::resources::dns::external::rdata::RDataValidationError; +use crate::resources::dns::internal::base::{Name, Text}; +use crate::validation; + + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "lowercase")] +pub enum ValueType { + Object, + Array, + String, + Number, + Null, + Bool, +} + +impl ValueType { + pub fn from_value(value: &Value) -> ValueType { + match value { + Value::Array(_) => ValueType::Array, + Value::Bool(_) => ValueType::Bool, + Value::Null => ValueType::Null, + Value::Number(_) => ValueType::Number, + Value::Object(_) => ValueType::Object, + Value::String(_) => ValueType::String, + } + } +} + +pub enum InputDataError { + TypeError { expected: ValueType, found: ValueType }, + MissingValue, +} + +pub trait FromValue: Sized { + fn from_value(value: Value) -> Result>; +} + +impl FromValue for Name { + fn from_value(value: Value) -> Result> { + let mut errors = Vec::new(); + + let value = check_type!(value, String, errors); + + if !errors.is_empty() { + return Err(errors); + } + + let name = push_error!( + validation::normalize_domain(&value.unwrap()), + errors + ); + + if errors.is_empty() { + Ok(Name::new(name.unwrap())) + } else { + Err(errors) + } + } +} + +impl FromValue for IpAddr { + fn from_value(value: Value) -> Result> { + let mut errors = Vec::new(); + + let address = check_type!(value, String, errors); + + let address = if let Some(address) = address { + // TODO: replace with custom validation + push_error!(address.parse::().map_err(|e| { + Error::from(RDataValidationError::IpAddress { input: address }) + .with_cause(&e.to_string()) + }), errors) + } else { + None + }; + + + if errors.is_empty() { + Ok(address.unwrap()) + } else { + Err(errors) + } + } +} + +impl FromValue for u32 { + fn from_value(value: Value) -> Result> { + let mut errors = Vec::new(); + + let number = check_type!(value, Number, errors); + + let address = if let Some(number) = number { + push_error!( + number.as_u64() + .ok_or(Error::from(RDataValidationError::Number { min: u32::MIN.into(), max: u32::MAX.into()})) + .and_then(|number| { + u32::try_from(number).map_err(|e| { + Error::from(RDataValidationError::Number { min: u32::MIN.into(), max: u32::MAX.into()}) + .with_cause(&e.to_string()) + }) + }), + errors + ) + } else { + None + }; + + + if errors.is_empty() { + Ok(address.unwrap()) + } else { + Err(errors) + } + } +} + +impl FromValue for u16 { + fn from_value(value: Value) -> Result> { + let mut errors = Vec::new(); + + let number = check_type!(value, Number, errors); + + let address = if let Some(number) = number { + push_error!( + number.as_u64() + .ok_or(Error::from(RDataValidationError::Number { min: u16::MIN.into(), max: u16::MAX.into()})) + .and_then(|number| { + u16::try_from(number).map_err(|e| { + Error::from(RDataValidationError::Number { min: u16::MIN.into(), max: u16::MAX.into()}) + .with_cause(&e.to_string()) + }) + }), + errors + ) + } else { + None + }; + + + if errors.is_empty() { + Ok(address.unwrap()) + } else { + Err(errors) + } + } +} + +impl FromValue for Vec { + fn from_value(value: serde_json::Value) -> Result> { + let mut errors = Vec::new(); + + let array = check_type!(value, Array, errors); + + if !errors.is_empty() { + return Err(errors); + } + let array = array.unwrap(); + + let mut list = Vec::new(); + + for (index, item) in array.into_iter().enumerate() { + let res = append_errors!( + T::from_value(item), + errors, + &format!("/{index}") + ); + + if let Some(item) = res { + list.push(item); + } + } + + if errors.is_empty() { + Ok(list) + } else { + Err(errors) + } + } +} + +impl FromValue for Text { + fn from_value(value: Value) -> Result> { + let mut errors = Vec::new(); + + let data = check_type!(value, String, errors); + + let data = if let Some(data) = data { + append_errors!(validation::parse_txt_data(&data), errors) + } else { + None + }; + + if errors.is_empty() { + Ok(Text ::new(data.unwrap())) + } else { + Err(errors) + } + } +} diff --git a/src/resources/dns/friendly/create.rs b/src/resources/dns/friendly/create.rs new file mode 100644 index 0000000..37b1797 --- /dev/null +++ b/src/resources/dns/friendly/create.rs @@ -0,0 +1,253 @@ +use serde::{Deserialize, Serialize}; + +use crate::validation; +use crate::errors::Error; +use crate::macros::{append_errors, check_type, get_object_value, push_error}; +use crate::resources::dns::internal::{self, Name}; + +use super::{rdata, FriendlyRType}; +use super::FromValue; + + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum ConfigurationType { + Mail, + Web, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct NewRecordQuery { + pub name: Option, + pub config: Option, + pub rtype: Option, +} + +impl NewRecordQuery { + pub fn validate(&mut self, zone_name: &str) -> Result<(), Vec> { + let mut errors = Vec::new(); + if let Some(input_name) = &self.name { + if input_name.is_empty() { + self.name = Some(zone_name.to_string()); + } else { + let new_name = format!("{input_name}.{zone_name}"); + let new_name = push_error!( + validation::normalize_domain(&new_name), + errors, + "/name" + ); + self.name = new_name; + } + } + + if errors.is_empty() { + Ok(()) + } else { + Err(errors) + } + } +} + +pub trait ToInternal { + fn internal(self, ttl: u32, node_name: Name) -> Vec; +} + +#[derive(Debug)] +pub struct NewRecord { + ttl: Option, + data: Option, +} + +impl FromValue for NewRecord { + fn from_value(value: tera::Value) -> Result> { + let mut errors = Vec::new(); + + let object = check_type!(value, Object, errors); + + if !errors.is_empty() { + return Err(errors); + } + + let mut object = object.unwrap(); + + let ttl = object.remove("ttl"); + let ttl = if let Some(ttl) = ttl { + append_errors!(FromValue::from_value(ttl), errors, "/ttl") + } else { + None + }; + + let data = object.remove("data"); + let data = if let Some(data) = data { + append_errors!(FromValue::from_value(data), errors, "/data") + } else { + None + }; + + if errors.is_empty() { + Ok(NewRecord { + ttl, + data, + }) + } else { + Err(errors) + } + + } +} + +impl ToInternal for NewRecord { + fn internal(self, ttl: u32, node_name: Name) -> Vec { + let ttl = self.ttl.unwrap_or(ttl); + + self.data + .map(|data| data.internal(ttl, node_name)) + .unwrap_or_default() + } +} + +#[derive(Debug)] +pub struct NewRequiredRecord { + ttl: Option, + data: T, +} + +impl FromValue for NewRequiredRecord { + fn from_value(value: tera::Value) -> Result> { + let mut errors = Vec::new(); + + let object = check_type!(value, Object, errors); + + if !errors.is_empty() { + return Err(errors); + } + + let mut object = object.unwrap(); + + let ttl = object.remove("ttl"); + let ttl = if let Some(ttl) = ttl { + append_errors!(FromValue::from_value(ttl), errors, "/ttl") + } else { + None + }; + + let data = get_object_value!(object, "data", errors); + let data = if let Some(data) = data { + append_errors!(FromValue::from_value(data), errors, "/data") + } else { + None + }; + + if errors.is_empty() { + Ok(NewRequiredRecord { + ttl, + data: data.unwrap(), + }) + } else { + Err(errors) + } + + } +} + +impl ToInternal for NewRequiredRecord { + fn internal(self, ttl: u32, node_name: Name) -> Vec { + let ttl = self.ttl.unwrap_or(ttl); + + self.data.internal(ttl, node_name) + } +} + +#[derive(Debug)] +pub struct NewSectionWeb { + addresses: NewRequiredRecord, +} + +impl FromValue for NewSectionWeb { + fn from_value(value: serde_json::Value) -> Result> { + let mut errors = Vec::new(); + + let object = check_type!(value, Object, errors); + + if !errors.is_empty() { + return Err(errors); + } + + let mut object = object.unwrap(); + + let addresses = get_object_value!(object, "addresses", errors); + let addresses = if let Some(addresses) = addresses { + append_errors!(FromValue::from_value(addresses), errors, "/addresses") + } else { + None + }; + + if errors.is_empty() { + Ok(NewSectionWeb { + addresses: addresses.unwrap(), + }) + } else { + Err(errors) + } + + } +} + +impl ToInternal for NewSectionWeb { + fn internal(self, ttl: u32, node_name: Name) -> Vec { + self.addresses.internal(ttl, node_name) + } +} + +#[derive(Debug)] +pub struct NewSectionMail { + mailservers: NewRequiredRecord, + spf: NewRecord, +} + +impl FromValue for NewSectionMail { + fn from_value(value: serde_json::Value) -> Result> { + let mut errors = Vec::new(); + + let object = check_type!(value, Object, errors); + + if !errors.is_empty() { + return Err(errors); + } + + let mut object = object.unwrap(); + + let mailservers = get_object_value!(object, "mailservers", errors); + let mailservers = if let Some(mailservers) = mailservers { + append_errors!(FromValue::from_value(mailservers), errors, "/mailservers") + } else { + None + }; + + let spf = get_object_value!(object, "spf", errors); + let spf = if let Some(spf) = spf { + append_errors!(FromValue::from_value(spf), errors, "/spf") + } else { + None + }; + + + if errors.is_empty() { + Ok(NewSectionMail { + mailservers: mailservers.unwrap(), + spf: spf.unwrap() + }) + } else { + Err(errors) + } + + } +} + +impl ToInternal for NewSectionMail { + fn internal(self, ttl: u32, node_name: Name) -> Vec { + let mut records = self.mailservers.internal(ttl, node_name.clone()); + records.extend(self.spf.internal(ttl, node_name)); + records + } +} diff --git a/src/resources/dns/friendly/mod.rs b/src/resources/dns/friendly/mod.rs index 3d9a147..20026c1 100644 --- a/src/resources/dns/friendly/mod.rs +++ b/src/resources/dns/friendly/mod.rs @@ -1,3 +1,9 @@ pub mod rdata; +pub mod record; +pub mod base; +pub mod create; pub use rdata::*; +pub use record::*; +pub use base::*; +pub use create::*; diff --git a/src/resources/dns/friendly/rdata.rs b/src/resources/dns/friendly/rdata.rs index 0323e0e..8362b9b 100644 --- a/src/resources/dns/friendly/rdata.rs +++ b/src/resources/dns/friendly/rdata.rs @@ -1,30 +1,14 @@ -use std::{collections::HashMap, hash::Hash}; +use std::hash::Hash; +use std::net::IpAddr; -use serde::{Deserialize, Serialize, Serializer}; +use serde::{Deserialize, Serialize}; -use crate::resources::dns::internal; +use crate::errors::Error; +use crate::resources::dns::internal::{self, Name, Text}; +use crate::macros::{append_errors, check_type, get_object_value}; +use super::{FromValue, ToInternal}; -#[derive(Debug, Deserialize, Serialize, Hash, Eq, PartialEq)] -#[serde(rename_all="lowercase")] -pub enum FriendlyRType { - Addresses, - Alias, - MailServers, - NameServers, - Service, - Spf, - Texts, -} - -#[derive(Debug, Deserialize, Serialize)] -#[serde(rename_all="lowercase")] -pub enum RecordSection { - Mail, - Web, - Services, - Miscellaneous, -} #[derive(Debug, Deserialize, Serialize)] pub enum FriendlyRData { @@ -48,226 +32,42 @@ pub enum FriendlyRDataAggregated { Texts(Texts), } -#[derive(Debug)] -pub struct FriendlyRecord { - ttl: i64, - data: FriendlyRDataAggregated, -} - -impl Serialize for FriendlyRecord { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - #[derive(Serialize)] - struct ExtendedRecord<'a> { - ttl: i64, - record_type: FriendlyRType, - record_section: RecordSection, - #[serde(flatten)] - data: &'a FriendlyRDataAggregated, - } - - let extended_record = ExtendedRecord { - ttl: self.ttl, - data: &self.data, - record_type: self.record_type(), - record_section: self.record_section(), - }; - - extended_record.serialize(serializer) - - } -} - -impl FriendlyRecord { - pub fn new(ttl: u32, data: FriendlyRDataAggregated) -> Self { - FriendlyRecord { - ttl: ttl.into(), - data, - } - } - - pub fn record_type(&self) -> FriendlyRType { - match self.data { - FriendlyRDataAggregated::Addresses(_) => FriendlyRType::Addresses, - FriendlyRDataAggregated::MailServers(_) => FriendlyRType::MailServers, - FriendlyRDataAggregated::NameServers(_) => FriendlyRType::NameServers, - FriendlyRDataAggregated::Service(_) => FriendlyRType::Service, - FriendlyRDataAggregated::Spf(_) => FriendlyRType::Spf, - FriendlyRDataAggregated::Texts(_) => FriendlyRType::Texts, - } - } - - pub fn record_section(&self) -> RecordSection { - match self.data { - FriendlyRDataAggregated::Addresses(_) => RecordSection::Web, - FriendlyRDataAggregated::MailServers(_) => RecordSection::Mail, - FriendlyRDataAggregated::NameServers(_) => RecordSection::Miscellaneous, - FriendlyRDataAggregated::Service(_) => RecordSection::Services, - FriendlyRDataAggregated::Spf(_) => RecordSection::Mail, - FriendlyRDataAggregated::Texts(_) => RecordSection::Miscellaneous, - } - } -} - -#[derive(Debug, Serialize)] -pub struct Node { - pub name: String, - pub records: Vec, -} - -impl Node { - fn new(name: String) -> Self { - Node { - name, - records: Vec::new(), - } - } -} - -#[derive(Debug, Serialize)] -pub struct FriendlyRecords { - records: Vec, - aliases: 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(); - let mut name_mapping: HashMap> = HashMap::new(); - let mut service_mapping: HashMap<(String, ServiceType), FriendlyRecord> = HashMap::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 (name, rdata) = rdata.unwrap(); - - if let FriendlyRData::Alias(alias) = rdata { - records.aliases.push(alias) - } else { - let node = name_mapping.entry(name.clone()).or_default(); - - match rdata { - FriendlyRData::Address(address) => { - let addresses = node.entry(FriendlyRType::Addresses).or_insert_with(|| { - FriendlyRecord::new(ttl, FriendlyRDataAggregated::Addresses(Addresses { - addresses: Vec::new() - })) - }); - - match addresses.data { - FriendlyRDataAggregated::Addresses(ref mut addresses) => addresses.addresses.push(address), - _ => unreachable!(), - }; - }, - FriendlyRData::MailServer(mailserver) => { - let mailservers = node.entry(FriendlyRType::MailServers).or_insert_with(|| { - FriendlyRecord::new(ttl, FriendlyRDataAggregated::MailServers(MailServers { - mailservers: Vec::new() - })) - }); - - match mailservers.data { - FriendlyRDataAggregated::MailServers(ref mut mailservers) => mailservers.mailservers.push(mailserver), - _ => unreachable!(), - }; - }, - FriendlyRData::Spf(spf) => { - node.insert(FriendlyRType::Spf, FriendlyRecord::new(ttl, FriendlyRDataAggregated::Spf(spf))); - }, - FriendlyRData::Service(service_single) => { - let service = service_mapping.entry((name.clone(), service_single.service_type.clone())) - .or_insert_with(|| { - FriendlyRecord::new(ttl, FriendlyRDataAggregated::Service(Service { - service_type: service_single.service_type, - service_targets: Vec::new(), - - })) - }); - - match service.data { - FriendlyRDataAggregated::Service(ref mut service) => service.service_targets.push(service_single.service_target), - _ => unreachable!(), - }; - }, - FriendlyRData::NameServer(nameserver) => { - // TODO: NS -> Skip if NS for zone (authority), create Delegation section with glue + DS for others (how to check if record is glue?) - let nameservers = node.entry(FriendlyRType::NameServers).or_insert_with(|| { - FriendlyRecord::new(ttl, FriendlyRDataAggregated::NameServers(NameServers { - nameservers: Vec::new() - })) - }); - - match nameservers.data { - FriendlyRDataAggregated::NameServers(ref mut nameservers) => nameservers.nameservers.push(nameserver), - _ => unreachable!(), - }; - }, - FriendlyRData::TextData(text) => { - let texts = node.entry(FriendlyRType::Texts).or_insert_with(|| { - FriendlyRecord::new(ttl, FriendlyRDataAggregated::Texts(Texts { - texts: Vec::new() - })) - }); - - match texts.data { - FriendlyRDataAggregated::Texts(ref mut texts) => texts.texts.push(text), - _ => unreachable!(), - }; - }, - FriendlyRData::Alias(_) => {}, - } - } - } - - let mut nodes: HashMap = HashMap::new(); - - for ((name, _), service) in service_mapping { - let node = nodes.entry(name.clone()) - .or_insert_with(|| Node::new(name)); - node.records.push(service); - } - - for (name, node_records) in name_mapping { - let node = nodes.entry(name.clone()) - .or_insert_with(|| Node::new(name)); - for (_, record) in node_records { - node.records.push(record); - } - } - - records.records = nodes.into_values().collect(); - - records.records.sort_by_key(|node| node.name.clone()); - - records - } -} - /* --------- RDATA --------- */ /* --------- Address --------- */ #[derive(Debug, Deserialize, Serialize)] pub struct Address { - pub address: String + pub address: IpAddr, +} + +impl FromValue for Address { + fn from_value(value: serde_json::Value) -> Result> { + let mut errors = Vec::new(); + + let object = check_type!(value, Object, errors); + + if !errors.is_empty() { + return Err(errors); + } + + let mut object = object.unwrap(); + + let address = get_object_value!(object, "address", errors); + let address = if let Some(address) = address { + append_errors!(FromValue::from_value(address), errors, "/address") + } else { + None + }; + + if errors.is_empty() { + Ok(Address { + address: address.unwrap(), + }) + } else { + Err(errors) + } + } } #[derive(Debug, Deserialize, Serialize)] @@ -275,6 +75,51 @@ pub struct Addresses { pub addresses: Vec
, } +impl FromValue for Addresses { + fn from_value(value: serde_json::Value) -> Result> { + let mut errors = Vec::new(); + + let object = check_type!(value, Object, errors); + + if !errors.is_empty() { + return Err(errors); + } + + let mut object = object.unwrap(); + + let addresses = get_object_value!(object, "addresses", errors); + let addresses = if let Some(addresses) = addresses { + append_errors!(FromValue::from_value(addresses), errors, "/addresses") + } else { + None + }; + + if errors.is_empty() { + Ok(Addresses { + addresses: addresses.unwrap(), + }) + } else { + Err(errors) + } + + } +} + +impl ToInternal for Addresses { + fn internal(self, ttl: u32, node_name: Name) -> Vec { + self.addresses.into_iter().map(|addr| { + internal::Record { + ttl, + name: node_name.clone(), + rdata: match addr.address { + IpAddr::V4(ip4) => internal::RData::A(internal::A::new(ip4)), + IpAddr::V6(ip6) => internal::RData::Aaaa(internal::Aaaa::new(ip6)), + } + } + }).collect() + } +} + /* --------- Service --------- */ #[derive(Debug, Deserialize, Serialize)] @@ -307,8 +152,45 @@ pub enum ServiceType { #[derive(Debug, Deserialize, Serialize)] pub struct MailServer { - pub preference: i64, - pub mail_exchanger: String, + pub preference: u16, + pub mail_exchanger: Name, +} + +impl FromValue for MailServer { + fn from_value(value: serde_json::Value) -> Result> { + let mut errors = Vec::new(); + + let object = check_type!(value, Object, errors); + + if !errors.is_empty() { + return Err(errors); + } + + let mut object = object.unwrap(); + + let preference = get_object_value!(object, "preference", errors); + let preference = if let Some(preference) = preference { + append_errors!(FromValue::from_value(preference), errors, "/preference") + } else { + None + }; + + let mail_exchanger = get_object_value!(object, "mail_exchanger", errors); + let mail_exchanger = if let Some(mail_exchanger) = mail_exchanger { + append_errors!(FromValue::from_value(mail_exchanger), errors, "/mail_exchanger") + } else { + None + }; + + if errors.is_empty() { + Ok(MailServer { + preference: preference.unwrap(), + mail_exchanger: mail_exchanger.unwrap(), + }) + } else { + Err(errors) + } + } } #[derive(Debug, Deserialize, Serialize)] @@ -316,6 +198,49 @@ pub struct MailServers { pub mailservers: Vec } +impl FromValue for MailServers { + fn from_value(value: serde_json::Value) -> Result> { + let mut errors = Vec::new(); + + let object = check_type!(value, Object, errors); + + if !errors.is_empty() { + return Err(errors); + } + + let mut object = object.unwrap(); + + let mailservers = get_object_value!(object, "mailservers", errors); + let mailservers = if let Some(mailservers) = mailservers { + append_errors!(FromValue::from_value(mailservers), errors, "/mailservers") + } else { + None + }; + + if errors.is_empty() { + Ok(MailServers { + mailservers: mailservers.unwrap(), + }) + } else { + Err(errors) + } + + } +} + +impl ToInternal for MailServers { + fn internal(self, ttl: u32, node_name: Name) -> Vec { + self.mailservers.into_iter().map(|mailserver| { + internal::Record { + ttl, + name: node_name.clone(), + rdata: internal::RData::Mx(internal::Mx::new(mailserver.mail_exchanger, mailserver.preference)), + } + }).collect() + } +} + + /* --------- NameServer --------- */ #[derive(Debug, Deserialize, Serialize)] @@ -344,7 +269,50 @@ pub struct Texts { #[derive(Debug, Deserialize, Serialize)] pub struct Spf { - pub policy: String + pub policy: Text +} + +impl FromValue for Spf { + fn from_value(value: serde_json::Value) -> Result> { + let mut errors = Vec::new(); + + let object = check_type!(value, Object, errors); + + if !errors.is_empty() { + return Err(errors); + } + + let mut object = object.unwrap(); + + let policy = get_object_value!(object, "policy", errors); + let policy = if let Some(policy) = policy { + append_errors!(FromValue::from_value(policy), errors, "/policy") + } else { + None + }; + + if errors.is_empty() { + Ok(Spf { + policy: policy.unwrap(), + }) + } else { + Err(errors) + } + } +} + + +impl ToInternal for Spf { + fn internal(self, ttl: u32, node_name: Name) -> Vec { + + vec![ + internal::Record { + ttl, + name: Name::new(format!("_spf.{node_name}")), + rdata: internal::RData::Txt(internal::Txt::new(self.policy)) + } + ] + } } /* --------- Alias --------- */ diff --git a/src/resources/dns/friendly/record.rs b/src/resources/dns/friendly/record.rs new file mode 100644 index 0000000..168818b --- /dev/null +++ b/src/resources/dns/friendly/record.rs @@ -0,0 +1,249 @@ +use std::collections::HashMap; +use std::hash::Hash; + +use serde::{Serialize, Deserialize, Serializer}; + +use crate::resources::dns::internal; + +use super::rdata::{ + FriendlyRDataAggregated, + FriendlyRData, + ServiceType, + self +}; + + +#[derive(Debug, Serialize)] +#[serde(rename_all="lowercase")] +pub enum RecordSection { + Mail, + Web, + Services, + Miscellaneous, +} + + +#[derive(Debug, Deserialize, Serialize, Hash, Eq, PartialEq)] +#[serde(rename_all="lowercase")] +pub enum FriendlyRType { + Addresses, + Alias, + MailServers, + NameServers, + Service, + Spf, + Texts, +} + +#[derive(Debug)] +pub struct FriendlyRecord { + ttl: u32, + data: FriendlyRDataAggregated, +} + +impl Serialize for FriendlyRecord { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + #[derive(Serialize)] + struct ExtendedRecord<'a> { + ttl: u32, + record_type: FriendlyRType, + record_section: RecordSection, + #[serde(flatten)] + data: &'a FriendlyRDataAggregated, + } + + let extended_record = ExtendedRecord { + ttl: self.ttl, + data: &self.data, + record_type: self.record_type(), + record_section: self.record_section(), + }; + + extended_record.serialize(serializer) + + } +} + +impl FriendlyRecord { + pub fn new(ttl: u32, data: FriendlyRDataAggregated) -> Self { + FriendlyRecord { + ttl, + data, + } + } + + pub fn record_type(&self) -> FriendlyRType { + match self.data { + FriendlyRDataAggregated::Addresses(_) => FriendlyRType::Addresses, + FriendlyRDataAggregated::MailServers(_) => FriendlyRType::MailServers, + FriendlyRDataAggregated::NameServers(_) => FriendlyRType::NameServers, + FriendlyRDataAggregated::Service(_) => FriendlyRType::Service, + FriendlyRDataAggregated::Spf(_) => FriendlyRType::Spf, + FriendlyRDataAggregated::Texts(_) => FriendlyRType::Texts, + } + } + + pub fn record_section(&self) -> RecordSection { + match self.data { + FriendlyRDataAggregated::Addresses(_) => RecordSection::Web, + FriendlyRDataAggregated::MailServers(_) => RecordSection::Mail, + FriendlyRDataAggregated::NameServers(_) => RecordSection::Miscellaneous, + FriendlyRDataAggregated::Service(_) => RecordSection::Services, + FriendlyRDataAggregated::Spf(_) => RecordSection::Mail, + FriendlyRDataAggregated::Texts(_) => RecordSection::Miscellaneous, + } + } +} + +#[derive(Debug, Serialize)] +pub struct Node { + pub name: String, + pub records: Vec, +} + +impl Node { + fn new(name: String) -> Self { + Node { + name, + records: Vec::new(), + } + } +} + +#[derive(Debug, Serialize)] +pub struct FriendlyRecords { + records: Vec, + aliases: 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(); + let mut name_mapping: HashMap> = HashMap::new(); + let mut service_mapping: HashMap<(String, ServiceType), FriendlyRecord> = HashMap::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 (name, rdata) = rdata.unwrap(); + + if let FriendlyRData::Alias(alias) = rdata { + records.aliases.push(alias) + } else { + let node = name_mapping.entry(name.clone()).or_default(); + + match rdata { + FriendlyRData::Address(address) => { + let addresses = node.entry(FriendlyRType::Addresses).or_insert_with(|| { + FriendlyRecord::new(ttl, FriendlyRDataAggregated::Addresses(rdata::Addresses { + addresses: Vec::new() + })) + }); + + match addresses.data { + FriendlyRDataAggregated::Addresses(ref mut addresses) => addresses.addresses.push(address), + _ => unreachable!(), + }; + }, + FriendlyRData::MailServer(mailserver) => { + let mailservers = node.entry(FriendlyRType::MailServers).or_insert_with(|| { + FriendlyRecord::new(ttl, FriendlyRDataAggregated::MailServers(rdata::MailServers { + mailservers: Vec::new() + })) + }); + + match mailservers.data { + FriendlyRDataAggregated::MailServers(ref mut mailservers) => mailservers.mailservers.push(mailserver), + _ => unreachable!(), + }; + }, + FriendlyRData::Spf(spf) => { + node.insert(FriendlyRType::Spf, FriendlyRecord::new(ttl, FriendlyRDataAggregated::Spf(spf))); + }, + FriendlyRData::Service(service_single) => { + let service = service_mapping.entry((name.clone(), service_single.service_type.clone())) + .or_insert_with(|| { + FriendlyRecord::new(ttl, FriendlyRDataAggregated::Service(rdata::Service { + service_type: service_single.service_type, + service_targets: Vec::new(), + + })) + }); + + match service.data { + FriendlyRDataAggregated::Service(ref mut service) => service.service_targets.push(service_single.service_target), + _ => unreachable!(), + }; + }, + FriendlyRData::NameServer(nameserver) => { + // TODO: NS -> Skip if NS for zone (authority), create Delegation section with glue + DS for others (how to check if record is glue?) + let nameservers = node.entry(FriendlyRType::NameServers).or_insert_with(|| { + FriendlyRecord::new(ttl, FriendlyRDataAggregated::NameServers(rdata::NameServers { + nameservers: Vec::new() + })) + }); + + match nameservers.data { + FriendlyRDataAggregated::NameServers(ref mut nameservers) => nameservers.nameservers.push(nameserver), + _ => unreachable!(), + }; + }, + FriendlyRData::TextData(text) => { + let texts = node.entry(FriendlyRType::Texts).or_insert_with(|| { + FriendlyRecord::new(ttl, FriendlyRDataAggregated::Texts(rdata::Texts { + texts: Vec::new() + })) + }); + + match texts.data { + FriendlyRDataAggregated::Texts(ref mut texts) => texts.texts.push(text), + _ => unreachable!(), + }; + }, + FriendlyRData::Alias(_) => {}, + } + } + } + + let mut nodes: HashMap = HashMap::new(); + + for ((name, _), service) in service_mapping { + let node = nodes.entry(name.clone()) + .or_insert_with(|| Node::new(name)); + node.records.push(service); + } + + for (name, node_records) in name_mapping { + let node = nodes.entry(name.clone()) + .or_insert_with(|| Node::new(name)); + for (_, record) in node_records { + node.records.push(record); + } + } + + records.records = nodes.into_values().collect(); + + records.records.sort_by_key(|node| node.name.clone()); + + records + } +} diff --git a/src/resources/dns/internal/base.rs b/src/resources/dns/internal/base.rs index 494bf10..e83fca3 100644 --- a/src/resources/dns/internal/base.rs +++ b/src/resources/dns/internal/base.rs @@ -1,6 +1,8 @@ use std::fmt; +use domain::base::scan::Symbol; +use serde::{Serialize, Deserialize, Serializer}; -#[derive(Clone, Eq, PartialEq)] +#[derive(Clone, Debug, Eq, PartialEq, Deserialize)] pub struct Name { name: String } @@ -17,6 +19,15 @@ impl Name { } } +impl Serialize for Name { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + impl fmt::Display for Name { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.name) @@ -88,3 +99,42 @@ impl PartialOrd for Rtype { Some(self.cmp(other)) } } + +#[derive(Clone, Debug, Deserialize)] +pub struct Text { + pub data: Vec +} + +impl Text { + pub fn new(data: Vec) -> Self { + Text { + data, + } + } + + pub fn bytes(self) -> Vec { + self.data + } +} + +impl Serialize for Text { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + +impl fmt::Display for Text { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + + for c in &self.data { + // Escapes '\' and non printable chars + let c = Symbol::display_from_octet(*c); + write!(f, "{}", c)?; + } + + Ok(()) + } +} diff --git a/src/resources/dns/internal/rdata.rs b/src/resources/dns/internal/rdata.rs index ff9107d..9a1198b 100644 --- a/src/resources/dns/internal/rdata.rs +++ b/src/resources/dns/internal/rdata.rs @@ -1,10 +1,10 @@ use std::net::{Ipv4Addr, Ipv6Addr}; -use super::{Name, Rtype}; +use super::{Name, Rtype, Text}; use crate::resources::dns::friendly; -#[derive(Clone)] +#[derive(Clone, Debug)] pub enum RData { A(A), Aaaa(Aaaa), @@ -45,33 +45,45 @@ impl RData { } } -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct A { pub address: Ipv4Addr, } impl A { + pub fn new(address: Ipv4Addr) -> Self { + A { + address + } + } + pub fn friendly(self, owner: &str) -> Option<(String, friendly::FriendlyRData)> { Some((owner.to_string(), friendly::FriendlyRData::Address(friendly::Address { - address: self.address.to_string() + address: self.address.into() }))) } } -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct Aaaa { pub address: Ipv6Addr, } impl Aaaa { + + pub fn new(address: Ipv6Addr) -> Self { + Aaaa { + address + } + } pub fn friendly(self, owner: &str) -> Option<(String, friendly::FriendlyRData)> { Some((owner.to_string(), friendly::FriendlyRData::Address(friendly::Address { - address: self.address.to_string() + address: self.address.into() }))) } } -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct Cname { pub target: Name, } @@ -86,22 +98,29 @@ impl Cname { } } -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct Mx { pub preference: u16, pub mail_exchanger: Name, } impl Mx { + pub fn new(mail_exchanger: Name, preference: u16) -> Self { + Mx { + mail_exchanger, + preference, + } + } + pub fn friendly(self, owner: &str) -> Option<(String, friendly::FriendlyRData)> { Some((owner.to_string(), friendly::FriendlyRData::MailServer(friendly::MailServer { - preference: self.preference.into(), - mail_exchanger: self.mail_exchanger.to_string(), + preference: self.preference, + mail_exchanger: self.mail_exchanger, }))) } } -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct Ns { pub target: Name, } @@ -114,12 +133,12 @@ impl Ns { } } -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct Ptr { pub target: Name, } -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct Soa { pub primary_server: Name, pub maintainer: Name, @@ -130,7 +149,7 @@ pub struct Soa { pub serial: u32, } -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct Srv { pub server: Name, pub port: u16, @@ -167,7 +186,15 @@ impl Srv { } } -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct Txt { pub text: Vec, } + +impl Txt { + pub fn new(text: Text) -> Self { + Txt { + text: text.bytes() + } + } +} diff --git a/src/resources/dns/internal/record.rs b/src/resources/dns/internal/record.rs index 2d5be24..0c8e6c8 100644 --- a/src/resources/dns/internal/record.rs +++ b/src/resources/dns/internal/record.rs @@ -1,14 +1,14 @@ use super::rdata::RData; use super::Name; -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct Record { pub name: Name, pub ttl: u32, pub rdata: RData } -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct RecordList { pub records: Vec, } diff --git a/src/routes/ui/zones.rs b/src/routes/ui/zones.rs index 6090b92..46bf01a 100644 --- a/src/routes/ui/zones.rs +++ b/src/routes/ui/zones.rs @@ -1,14 +1,15 @@ use axum::extract::{Query, Path, State, OriginalUri}; use axum::Extension; -use serde::{Deserialize, Serialize}; use serde_json::{Value, json}; use unic_langid::LanguageIdentifier; -use crate::validation; +use crate::form::Node; +use crate::macros::append_errors; use crate::AppState; -use crate::errors::Error; +use crate::errors::{Error, error_map}; use crate::template::Template; -use crate::resources::dns::friendly; +use crate::resources::dns::friendly::{self, NewRecordQuery, ConfigurationType, FromValue, NewSectionMail, NewSectionWeb, ToInternal}; +use crate::resources::dns::internal; pub async fn get_records_page( Path(zone_name): Path, @@ -32,54 +33,26 @@ pub async fn get_records_page( )) } -#[derive(Deserialize)] -pub struct NewRecordQuery { - subdomain: Option, - config: Option, - rtype: Option, -} - -#[derive(Serialize, Deserialize)] -#[serde(rename_all = "lowercase")] -pub enum ConfigurationType { - Mail, - Web, -} pub async fn get_new_record_page( Path(zone_name): Path, State(app): State, - Query(params): Query, + Query(mut params): Query, OriginalUri(url): OriginalUri, Extension(lang): Extension, ) -> 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) - }; - - println!("{}", lang); + let mut errors = Vec::new(); + append_errors!(params.validate(&zone.name), errors); Ok(Template::new( "pages/new_record.html", app.template_engine, json!({ "current_zone": zone.name, - "new_record_name": name, - "domain_error": domain_error, + "new_record_name": params.name, + "errors": error_map(errors), "config": params.config, "rtype": params.rtype, "url": url.to_string(), @@ -87,3 +60,70 @@ pub async fn get_new_record_page( }) )) } + +pub async fn post_new_record( + Path(zone_name): Path, + State(app): State, + Query(mut params): Query, + OriginalUri(url): OriginalUri, + Extension(lang): Extension, + form: Node, +) -> Result, Error> { + let zone = app.db.get_zone_by_name(&zone_name).await?; + let mut errors = Vec::new(); + append_errors!(params.validate(&zone.name), errors); + + if !errors.is_empty() || params.name.is_none() || !(params.config.is_none() ^ params.config.is_some()) { + // TODO: return 404 + todo!() + } + + let name = params.name.clone().unwrap(); + let input_data = form.to_json_value(); + + let new_records = if errors.is_empty() { + let name = internal::Name::new(name); + + let new_records = if let Some(config_type) = params.config.clone() { + match config_type { + ConfigurationType::Mail => { + NewSectionMail::from_value(input_data.clone()) + .map(|section| section.internal(3600, name)) + }, + ConfigurationType::Web => { + NewSectionWeb::from_value(input_data.clone()) + .map(|section| section.internal(3600, name)) + }, + } + } else if let Some(_rtype) = params.rtype { + unimplemented!() + } else { + unreachable!() + }; + + append_errors!(new_records, errors) + } else { + None + }; + + if !errors.is_empty() { + Ok(Template::new( + "pages/new_record.html", + app.template_engine, + json!({ + "current_zone": zone.name, + "new_record_name": params.name, + "input_data": input_data, + "errors": error_map(errors), + "config": params.config, + "rtype": params.rtype, + "url": url.to_string(), + "lang": lang.to_string(), + }) + )) + } else { + println!("{:#?}", new_records); + todo!() + } + +} diff --git a/src/validation.rs b/src/validation.rs index 6ae52ba..1d1984b 100644 --- a/src/validation.rs +++ b/src/validation.rs @@ -79,7 +79,7 @@ pub fn parse_txt_data(text: &str) -> Result, Vec> { digits.insert(0, ch); if digits.len() < 3 { errors.push(Error::from(TxtParseError::BadEscapeDigitsTooShort { sequence: String::from_iter(digits) })) - } else if digits.iter().all(|c| c.is_ascii_digit()) { + } else if !digits.iter().all(|c| c.is_ascii_digit()) { errors.push(Error::from(TxtParseError::BadEscapeDigitsNotDigits { sequence: String::from_iter(digits) })) } else { let index = { @@ -91,6 +91,8 @@ pub fn parse_txt_data(text: &str) -> Result, Vec> { if index > 255 { errors.push(Error::from(TxtParseError::BadEscapeDigitIndexTooHigh { sequence: String::from_iter(digits) })) } + + data.push(index as u8) } } else if printable(ch) { data.push(ch as u8) diff --git a/templates/pages/new_record.html b/templates/pages/new_record.html index 050f2d1..87cb7a6 100644 --- a/templates/pages/new_record.html +++ b/templates/pages/new_record.html @@ -4,7 +4,7 @@ {% block main %}

Create a new record in zone {{ current_zone }}

- +{{ errors | json_encode(pretty=true) }} {% if not new_record_name or (new_record_name and domain_error) %} {% include "pages/new_record/choose_name.html" %} {% elif not config and not rtype %} diff --git a/templates/pages/new_record/choose_name.html b/templates/pages/new_record/choose_name.html index 08b5fd9..1ee7bd3 100644 --- a/templates/pages/new_record/choose_name.html +++ b/templates/pages/new_record/choose_name.html @@ -1,19 +1,21 @@

Choose the name of the new record

- + + {% set domain_error = errors | get(key="/name", default="") %}
.{{ current_zone }}
{% if domain_error %} -

{{ domain_error.description }}

+

{{ domain_error.description }}

{% endif %} -

Only the subdomain, without the parent domain. For instance, "www" to create the subdomain "www.{{ current_zone }}".

+

Only the subdomain, without the parent domain. For instance, "www" to create the subdomain "www.{{ current_zone }}".

diff --git a/templates/pages/new_record/configure_record.html b/templates/pages/new_record/configure_record.html index ffb93c6..80143f4 100644 --- a/templates/pages/new_record/configure_record.html +++ b/templates/pages/new_record/configure_record.html @@ -2,43 +2,84 @@

Configure a web site for the domain {{ new_record_name }}

-
+

Web servers

+ + +
- - + +
- + {% for address in input_data.addresses.data.addresses | default(value=[""]) %} + {% set address_error = errors | get(key="/addresses/data/addresses/" ~ loop.index0 ~ "/address", default="") %} +
+ + + {% if address_error %} +

+ {{ tr( + msg="record-input-addresses", + attr="error-" ~ address_error.code | replace(from=":", to="-"), + lang=lang) }} +

+ {% endif %} +
+ {% endfor %} + +
{% elif config == "mail" %}

Configure e-mails for the domain {{ new_record_name }}

-
+

Mail servers

-
- Mail server #1 + -
-
- - -
+ {% for mailserver in input_data.mailservers.data.mailservers | default(value=[""]) %} +
+ Mail server #{{ loop.index }} +
+
+ + +
-
- - +
+ + +
-
-
+
+ {% endfor %}

Security

+
- - + +