wip: create record form

This commit is contained in:
Hannaeko 2025-05-13 20:23:26 +02:00
parent cfdd9afc0e
commit 08b21ac010
23 changed files with 1541 additions and 322 deletions

1
Cargo.lock generated
View file

@ -775,6 +775,7 @@ dependencies = [
"rusqlite", "rusqlite",
"serde", "serde",
"serde_json", "serde_json",
"serde_urlencoded",
"tera", "tera",
"tokio", "tokio",
"tower", "tower",

View file

@ -28,3 +28,4 @@ tower-http = { version = "0.6", default-features = false, features = [ "fs" ]}
fluent-bundle = "0.15.3" fluent-bundle = "0.15.3"
unic-langid = "*" unic-langid = "*"
tower = "*" tower = "*"
serde_urlencoded = "*"

View file

@ -29,3 +29,11 @@ zone-content-record-type-service =
zone-content-new-record-button = New record zone-content-new-record-button = New record
## Create 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 <code>198.51.100.3</code>, or an IPv6
address, like <code>2001:db8:cafe:bc68::2</code>.
button-save-configuration = Save configuration

View file

@ -29,3 +29,11 @@ zone-content-record-type-service =
zone-content-new-record-button = Nouvel enregistrement zone-content-new-record-button = Nouvel enregistrement
## Create record ## 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 <code>198.51.100.3</code>, soit une adresse IPv6,
comme <code>2001:db8:cafe:bc68::2</code>.
button-save-configuration = Sauvegarder la configuration

View file

@ -1,4 +1,5 @@
use std::fmt; use std::fmt;
use std::collections::HashMap;
use axum::http::{self, StatusCode}; use axum::http::{self, StatusCode};
use axum::response::{AppendHeaders, IntoResponse, Response}; use axum::response::{AppendHeaders, IntoResponse, Response};
@ -10,6 +11,7 @@ use serde_json::{Value, json};
use crate::dns::DnsDriverError; use crate::dns::DnsDriverError;
use crate::resources::dns::external::rdata::RDataValidationError; use crate::resources::dns::external::rdata::RDataValidationError;
use crate::resources::dns::external::record::{RecordError, RecordValidationError}; use crate::resources::dns::external::record::{RecordError, RecordValidationError};
use crate::resources::dns::friendly::InputDataError;
use crate::resources::zone::ZoneError; use crate::resources::zone::ZoneError;
use crate::validation::{DomainValidationError, TxtParseError}; use crate::validation::{DomainValidationError, TxtParseError};
use crate::template::TemplateError; use crate::template::TemplateError;
@ -41,6 +43,13 @@ pub fn serialize_status<S>(status: &Option<StatusCode>, serializer: S) -> Result
} }
} }
pub fn error_map(errors: Vec<Error>) -> HashMap<String, Error> {
errors.into_iter()
.filter_map(|mut error|
error.path.take().map(|path| (path, error))
).collect()
}
impl Error { impl Error {
pub fn new(code: &str, description: &str) -> Self { pub fn new(code: &str, description: &str) -> Self {
Error { Error {
@ -338,6 +347,19 @@ impl From<RDataValidationError> for Error {
"input": input "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<ProtoDnsError> for Error {
} }
} }
} }
impl From<InputDataError> 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.")
}
}
}
}

250
src/form.rs Normal file
View file

@ -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<S> FromRequest<S> for Node
where
S: Send + Sync,
{
type Rejection = Response;
async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
let Form(data): Form<Vec<(String, String)>> = 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<String, Node>),
Sequence(Sequence)
}
impl Node {
pub fn from_key_value(data: Vec<(String, String)>) -> Result<Node, FormError> {
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<Key> {
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::<usize>() {
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<Node>),
ExplicitIndex(HashMap<usize, Node>),
}
impl Sequence {
pub fn to_vec(self) -> Vec<Node> {
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);
}
}

View file

@ -110,6 +110,10 @@ impl tera::Function for Localization {
Ok(tera::Value::from(localized_message)) 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<FluentValue<'s>> { fn fluent_value_from_tera<'s>(value: &'s tera::Value, name: &str) -> tera::Result<FluentValue<'s>> {

View file

@ -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::<i64>().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 append_errors;
pub(crate) use push_error; pub(crate) use push_error;
pub(crate) use check_type;
pub(crate) use get_object_value;

View file

@ -8,6 +8,7 @@ mod macros;
mod template; mod template;
mod proto; mod proto;
mod localization; mod localization;
mod form;
use std::sync::Arc; use std::sync::Arc;
@ -66,6 +67,7 @@ async fn main() {
/* ----- UI ----- */ /* ----- UI ----- */
.route("/zones/{zone_name}/records", routing::get(routes::ui::zones::get_records_page)) .route("/zones/{zone_name}/records", routing::get(routes::ui::zones::get_records_page))
.route("/zones/{zone_name}/records/new", routing::get(routes::ui::zones::get_new_record_page)) .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")) .nest_service("/assets", ServeDir::new("assets"))
.with_state(app_state) .with_state(app_state)
.layer(localization.language_middleware("en".parse().unwrap())); .layer(localization.language_middleware("en".parse().unwrap()));

View file

@ -13,6 +13,8 @@ use crate::resources::dns::internal;
pub enum RDataValidationError { pub enum RDataValidationError {
Ip4Address { input: String }, Ip4Address { input: String },
Ip6Address { input: String }, Ip6Address { input: String },
IpAddress { input: String },
Number { min: i128, max: i128 }
} }
/// Type used to serialize / deserialize resource records data to response / request /// Type used to serialize / deserialize resource records data to response / request

View file

@ -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<Self, Vec<Error>>;
}
impl FromValue for Name {
fn from_value(value: Value) -> Result<Self, Vec<Error>> {
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<Self, Vec<Error>> {
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::<IpAddr>().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<Self, Vec<Error>> {
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<Self, Vec<Error>> {
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<T: FromValue> FromValue for Vec<T> {
fn from_value(value: serde_json::Value) -> Result<Self, Vec<Error>> {
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<Self, Vec<Error>> {
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)
}
}
}

View file

@ -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<String>,
pub config: Option<ConfigurationType>,
pub rtype: Option<FriendlyRType>,
}
impl NewRecordQuery {
pub fn validate(&mut self, zone_name: &str) -> Result<(), Vec<Error>> {
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<internal::Record>;
}
#[derive(Debug)]
pub struct NewRecord<T> {
ttl: Option<u32>,
data: Option<T>,
}
impl<T: FromValue> FromValue for NewRecord<T> {
fn from_value(value: tera::Value) -> Result<Self, Vec<crate::errors::Error>> {
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<T: ToInternal> ToInternal for NewRecord<T> {
fn internal(self, ttl: u32, node_name: Name) -> Vec<internal::Record> {
let ttl = self.ttl.unwrap_or(ttl);
self.data
.map(|data| data.internal(ttl, node_name))
.unwrap_or_default()
}
}
#[derive(Debug)]
pub struct NewRequiredRecord<T> {
ttl: Option<u32>,
data: T,
}
impl<T: FromValue> FromValue for NewRequiredRecord<T> {
fn from_value(value: tera::Value) -> Result<Self, Vec<crate::errors::Error>> {
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<T: ToInternal> ToInternal for NewRequiredRecord<T> {
fn internal(self, ttl: u32, node_name: Name) -> Vec<internal::Record> {
let ttl = self.ttl.unwrap_or(ttl);
self.data.internal(ttl, node_name)
}
}
#[derive(Debug)]
pub struct NewSectionWeb {
addresses: NewRequiredRecord<rdata::Addresses>,
}
impl FromValue for NewSectionWeb {
fn from_value(value: serde_json::Value) -> Result<Self, Vec<Error>> {
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<internal::Record> {
self.addresses.internal(ttl, node_name)
}
}
#[derive(Debug)]
pub struct NewSectionMail {
mailservers: NewRequiredRecord<rdata::MailServers>,
spf: NewRecord<rdata::Spf>,
}
impl FromValue for NewSectionMail {
fn from_value(value: serde_json::Value) -> Result<Self, Vec<Error>> {
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<internal::Record> {
let mut records = self.mailservers.internal(ttl, node_name.clone());
records.extend(self.spf.internal(ttl, node_name));
records
}
}

View file

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

View file

@ -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)] #[derive(Debug, Deserialize, Serialize)]
pub enum FriendlyRData { pub enum FriendlyRData {
@ -48,226 +32,42 @@ pub enum FriendlyRDataAggregated {
Texts(Texts), Texts(Texts),
} }
#[derive(Debug)]
pub struct FriendlyRecord {
ttl: i64,
data: FriendlyRDataAggregated,
}
impl Serialize for FriendlyRecord {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
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<FriendlyRecord>,
}
impl Node {
fn new(name: String) -> Self {
Node {
name,
records: Vec::new(),
}
}
}
#[derive(Debug, Serialize)]
pub struct FriendlyRecords {
records: Vec<Node>,
aliases: Vec<Alias>,
}
impl FriendlyRecords {
pub fn new() -> Self {
FriendlyRecords {
records: Vec::new(),
aliases: Vec::new(),
}
}
}
impl From<internal::RecordList> for FriendlyRecords {
fn from(value: internal::RecordList) -> Self {
let mut records = FriendlyRecords::new();
let mut name_mapping: HashMap<String, HashMap<FriendlyRType, FriendlyRecord>> = 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<String, Node> = 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 --------- */ /* --------- RDATA --------- */
/* --------- Address --------- */ /* --------- Address --------- */
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Deserialize, Serialize)]
pub struct Address { pub struct Address {
pub address: String pub address: IpAddr,
}
impl FromValue for Address {
fn from_value(value: serde_json::Value) -> Result<Self, Vec<Error>> {
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)] #[derive(Debug, Deserialize, Serialize)]
@ -275,6 +75,51 @@ pub struct Addresses {
pub addresses: Vec<Address>, pub addresses: Vec<Address>,
} }
impl FromValue for Addresses {
fn from_value(value: serde_json::Value) -> Result<Self, Vec<Error>> {
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<internal::Record> {
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 --------- */ /* --------- Service --------- */
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Deserialize, Serialize)]
@ -307,8 +152,45 @@ pub enum ServiceType {
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Deserialize, Serialize)]
pub struct MailServer { pub struct MailServer {
pub preference: i64, pub preference: u16,
pub mail_exchanger: String, pub mail_exchanger: Name,
}
impl FromValue for MailServer {
fn from_value(value: serde_json::Value) -> Result<Self, Vec<Error>> {
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)] #[derive(Debug, Deserialize, Serialize)]
@ -316,6 +198,49 @@ pub struct MailServers {
pub mailservers: Vec<MailServer> pub mailservers: Vec<MailServer>
} }
impl FromValue for MailServers {
fn from_value(value: serde_json::Value) -> Result<Self, Vec<Error>> {
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<internal::Record> {
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 --------- */ /* --------- NameServer --------- */
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Deserialize, Serialize)]
@ -344,7 +269,50 @@ pub struct Texts {
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Deserialize, Serialize)]
pub struct Spf { pub struct Spf {
pub policy: String pub policy: Text
}
impl FromValue for Spf {
fn from_value(value: serde_json::Value) -> Result<Self, Vec<Error>> {
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<internal::Record> {
vec![
internal::Record {
ttl,
name: Name::new(format!("_spf.{node_name}")),
rdata: internal::RData::Txt(internal::Txt::new(self.policy))
}
]
}
} }
/* --------- Alias --------- */ /* --------- Alias --------- */

View file

@ -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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
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<FriendlyRecord>,
}
impl Node {
fn new(name: String) -> Self {
Node {
name,
records: Vec::new(),
}
}
}
#[derive(Debug, Serialize)]
pub struct FriendlyRecords {
records: Vec<Node>,
aliases: Vec<rdata::Alias>,
}
impl FriendlyRecords {
pub fn new() -> Self {
FriendlyRecords {
records: Vec::new(),
aliases: Vec::new(),
}
}
}
impl From<internal::RecordList> for FriendlyRecords {
fn from(value: internal::RecordList) -> Self {
let mut records = FriendlyRecords::new();
let mut name_mapping: HashMap<String, HashMap<FriendlyRType, FriendlyRecord>> = 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<String, Node> = 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
}
}

View file

@ -1,6 +1,8 @@
use std::fmt; 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 { pub struct Name {
name: String name: String
} }
@ -17,6 +19,15 @@ impl Name {
} }
} }
impl Serialize for Name {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
impl fmt::Display for Name { impl fmt::Display for Name {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.name) write!(f, "{}", self.name)
@ -88,3 +99,42 @@ impl PartialOrd for Rtype {
Some(self.cmp(other)) Some(self.cmp(other))
} }
} }
#[derive(Clone, Debug, Deserialize)]
pub struct Text {
pub data: Vec<u8>
}
impl Text {
pub fn new(data: Vec<u8>) -> Self {
Text {
data,
}
}
pub fn bytes(self) -> Vec<u8> {
self.data
}
}
impl Serialize for Text {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
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(())
}
}

View file

@ -1,10 +1,10 @@
use std::net::{Ipv4Addr, Ipv6Addr}; use std::net::{Ipv4Addr, Ipv6Addr};
use super::{Name, Rtype}; use super::{Name, Rtype, Text};
use crate::resources::dns::friendly; use crate::resources::dns::friendly;
#[derive(Clone)] #[derive(Clone, Debug)]
pub enum RData { pub enum RData {
A(A), A(A),
Aaaa(Aaaa), Aaaa(Aaaa),
@ -45,33 +45,45 @@ impl RData {
} }
} }
#[derive(Clone)] #[derive(Clone, Debug)]
pub struct A { pub struct A {
pub address: Ipv4Addr, pub address: Ipv4Addr,
} }
impl A { impl A {
pub fn new(address: Ipv4Addr) -> Self {
A {
address
}
}
pub fn friendly(self, owner: &str) -> Option<(String, friendly::FriendlyRData)> { pub fn friendly(self, owner: &str) -> Option<(String, friendly::FriendlyRData)> {
Some((owner.to_string(), friendly::FriendlyRData::Address(friendly::Address { 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 struct Aaaa {
pub address: Ipv6Addr, pub address: Ipv6Addr,
} }
impl Aaaa { impl Aaaa {
pub fn new(address: Ipv6Addr) -> Self {
Aaaa {
address
}
}
pub fn friendly(self, owner: &str) -> Option<(String, friendly::FriendlyRData)> { pub fn friendly(self, owner: &str) -> Option<(String, friendly::FriendlyRData)> {
Some((owner.to_string(), friendly::FriendlyRData::Address(friendly::Address { 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 struct Cname {
pub target: Name, pub target: Name,
} }
@ -86,22 +98,29 @@ impl Cname {
} }
} }
#[derive(Clone)] #[derive(Clone, Debug)]
pub struct Mx { pub struct Mx {
pub preference: u16, pub preference: u16,
pub mail_exchanger: Name, pub mail_exchanger: Name,
} }
impl Mx { 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)> { pub fn friendly(self, owner: &str) -> Option<(String, friendly::FriendlyRData)> {
Some((owner.to_string(), friendly::FriendlyRData::MailServer(friendly::MailServer { Some((owner.to_string(), friendly::FriendlyRData::MailServer(friendly::MailServer {
preference: self.preference.into(), preference: self.preference,
mail_exchanger: self.mail_exchanger.to_string(), mail_exchanger: self.mail_exchanger,
}))) })))
} }
} }
#[derive(Clone)] #[derive(Clone, Debug)]
pub struct Ns { pub struct Ns {
pub target: Name, pub target: Name,
} }
@ -114,12 +133,12 @@ impl Ns {
} }
} }
#[derive(Clone)] #[derive(Clone, Debug)]
pub struct Ptr { pub struct Ptr {
pub target: Name, pub target: Name,
} }
#[derive(Clone)] #[derive(Clone, Debug)]
pub struct Soa { pub struct Soa {
pub primary_server: Name, pub primary_server: Name,
pub maintainer: Name, pub maintainer: Name,
@ -130,7 +149,7 @@ pub struct Soa {
pub serial: u32, pub serial: u32,
} }
#[derive(Clone)] #[derive(Clone, Debug)]
pub struct Srv { pub struct Srv {
pub server: Name, pub server: Name,
pub port: u16, pub port: u16,
@ -167,7 +186,15 @@ impl Srv {
} }
} }
#[derive(Clone)] #[derive(Clone, Debug)]
pub struct Txt { pub struct Txt {
pub text: Vec<u8>, pub text: Vec<u8>,
} }
impl Txt {
pub fn new(text: Text) -> Self {
Txt {
text: text.bytes()
}
}
}

View file

@ -1,14 +1,14 @@
use super::rdata::RData; use super::rdata::RData;
use super::Name; use super::Name;
#[derive(Clone)] #[derive(Clone, Debug)]
pub struct Record { pub struct Record {
pub name: Name, pub name: Name,
pub ttl: u32, pub ttl: u32,
pub rdata: RData pub rdata: RData
} }
#[derive(Clone)] #[derive(Clone, Debug)]
pub struct RecordList { pub struct RecordList {
pub records: Vec<Record>, pub records: Vec<Record>,
} }

View file

@ -1,14 +1,15 @@
use axum::extract::{Query, Path, State, OriginalUri}; use axum::extract::{Query, Path, State, OriginalUri};
use axum::Extension; use axum::Extension;
use serde::{Deserialize, Serialize};
use serde_json::{Value, json}; use serde_json::{Value, json};
use unic_langid::LanguageIdentifier; use unic_langid::LanguageIdentifier;
use crate::validation; use crate::form::Node;
use crate::macros::append_errors;
use crate::AppState; use crate::AppState;
use crate::errors::Error; use crate::errors::{Error, error_map};
use crate::template::Template; 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( pub async fn get_records_page(
Path(zone_name): Path<String>, Path(zone_name): Path<String>,
@ -32,54 +33,26 @@ pub async fn get_records_page(
)) ))
} }
#[derive(Deserialize)]
pub struct NewRecordQuery {
subdomain: Option<String>,
config: Option<ConfigurationType>,
rtype: Option<friendly::FriendlyRType>,
}
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ConfigurationType {
Mail,
Web,
}
pub async fn get_new_record_page( pub async fn get_new_record_page(
Path(zone_name): Path<String>, Path(zone_name): Path<String>,
State(app): State<AppState>, State(app): State<AppState>,
Query(params): Query<NewRecordQuery>, Query(mut params): Query<NewRecordQuery>,
OriginalUri(url): OriginalUri, OriginalUri(url): OriginalUri,
Extension(lang): Extension<LanguageIdentifier>, Extension(lang): Extension<LanguageIdentifier>,
) -> Result<Template<'static, Value>, Error> { ) -> Result<Template<'static, Value>, Error> {
let zone = app.db.get_zone_by_name(&zone_name).await?; let zone = app.db.get_zone_by_name(&zone_name).await?;
// Syntax validation of the subdomain let mut errors = Vec::new();
let (name, domain_error) = if let Some(input_name) = params.subdomain { append_errors!(params.validate(&zone.name), errors);
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);
Ok(Template::new( Ok(Template::new(
"pages/new_record.html", "pages/new_record.html",
app.template_engine, app.template_engine,
json!({ json!({
"current_zone": zone.name, "current_zone": zone.name,
"new_record_name": name, "new_record_name": params.name,
"domain_error": domain_error, "errors": error_map(errors),
"config": params.config, "config": params.config,
"rtype": params.rtype, "rtype": params.rtype,
"url": url.to_string(), "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<String>,
State(app): State<AppState>,
Query(mut params): Query<NewRecordQuery>,
OriginalUri(url): OriginalUri,
Extension(lang): Extension<LanguageIdentifier>,
form: Node,
) -> Result<Template<'static, Value>, 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!()
}
}

View file

@ -79,7 +79,7 @@ pub fn parse_txt_data(text: &str) -> Result<Vec<u8>, Vec<Error>> {
digits.insert(0, ch); digits.insert(0, ch);
if digits.len() < 3 { if digits.len() < 3 {
errors.push(Error::from(TxtParseError::BadEscapeDigitsTooShort { sequence: String::from_iter(digits) })) 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) })) errors.push(Error::from(TxtParseError::BadEscapeDigitsNotDigits { sequence: String::from_iter(digits) }))
} else { } else {
let index = { let index = {
@ -91,6 +91,8 @@ pub fn parse_txt_data(text: &str) -> Result<Vec<u8>, Vec<Error>> {
if index > 255 { if index > 255 {
errors.push(Error::from(TxtParseError::BadEscapeDigitIndexTooHigh { sequence: String::from_iter(digits) })) errors.push(Error::from(TxtParseError::BadEscapeDigitIndexTooHigh { sequence: String::from_iter(digits) }))
} }
data.push(index as u8)
} }
} else if printable(ch) { } else if printable(ch) {
data.push(ch as u8) data.push(ch as u8)

View file

@ -4,7 +4,7 @@
{% block main %} {% block main %}
<h1>Create a new record in zone {{ current_zone }}</h1> <h1>Create a new record in zone {{ current_zone }}</h1>
{{ errors | json_encode(pretty=true) }}
{% if not new_record_name or (new_record_name and domain_error) %} {% if not new_record_name or (new_record_name and domain_error) %}
{% include "pages/new_record/choose_name.html" %} {% include "pages/new_record/choose_name.html" %}
{% elif not config and not rtype %} {% elif not config and not rtype %}

View file

@ -1,19 +1,21 @@
<h2>Choose the name of the new record</h2> <h2>Choose the name of the new record</h2>
<form action="" method="GET"> <form action="" method="GET">
<label for="subdomain">Name of the new record</label> <label for="name">Name of the new record</label>
{% set domain_error = errors | get(key="/name", default="") %}
<div class="input-group"> <div class="input-group">
<input <input
type="text" type="text"
name="subdomain" name="name"
id="subdomain" id="name"
{% if domain_error %}aria-invalid="true"{% endif %} {% if domain_error %}aria-invalid="true"{% endif %}
aria-describedby="subdomain-help{% if domain_error %} subdomain-error{% endif %}" aria-describedby="{% if domain_error %}name-error {% endif %}subdomain-help"
value="{{ new_record_name | default(value="") }}"
> >
<span>.{{ current_zone }}</span> <span>.{{ current_zone }}</span>
</div> </div>
{% if domain_error %} {% if domain_error %}
<p class="error" id="subdomain-error">{{ domain_error.description }}</p> <p class="error" id="name-error">{{ domain_error.description }}</p>
{% endif %} {% endif %}
<p id="subdomain-help">Only the subdomain, without the parent domain. For instance, "www" to create the subdomain "www.{{ current_zone }}".</p> <p id="name-help">Only the subdomain, without the parent domain. For instance, "www" to create the subdomain "www.{{ current_zone }}".</p>
<button type="submit">Next step</button> <button type="submit">Next step</button>
</form> </form>

View file

@ -2,43 +2,84 @@
<h2>Configure a web site for the domain <strong>{{ new_record_name }}</strong></h2> <h2>Configure a web site for the domain <strong>{{ new_record_name }}</strong></h2>
<form> <form method="post" action="">
<h3>Web servers</h3> <h3>Web servers</h3>
<input name="addresses[_exist]" type="hidden" value="true">
<div class="form-input"> <div class="form-input">
<label for="record-0-addresses-address-0">IP Address #1</label> <label for="addresses-ttl">Duration in cache (TTL) (optional, default to 1 hour)</label>
<input name="records[0][addresses][addresses][0][address]" id="record-0-addresses-address-0" type="text"> <input id="addresses-ttl" name="addresses[ttl]" type="text">
</div> </div>
<button type="submit">Save configuration</button> {% for address in input_data.addresses.data.addresses | default(value=[""]) %}
{% set address_error = errors | get(key="/addresses/data/addresses/" ~ loop.index0 ~ "/address", default="") %}
<div class="form-input">
<label for="address-{{ loop.index0 }}">
{{ tr(msg="record-input-addresses", attr="input-label", index=loop.index, lang=lang) }}
</label>
<input
type="text"
name="addresses[data][addresses][{{ loop.index0 }}][address]"
id="address-{{ loop.index0 }}"
{% if domain_error %}aria-invalid="true"{% endif %}
value="{{ address.address | default(value="") }}"
>
{% if address_error %}
<p class="error" id="address-{{ loop.index0 }}-error">
{{ tr(
msg="record-input-addresses",
attr="error-" ~ address_error.code | replace(from=":", to="-"),
lang=lang) }}
</p>
{% endif %}
</div>
{% endfor %}
<button type="submit">{{ tr(msg="button-save-configuration", lang=lang) }}</button>
</form> </form>
{% elif config == "mail" %} {% elif config == "mail" %}
<h2>Configure e-mails for the domain <strong>{{ new_record_name }}</strong></h2> <h2>Configure e-mails for the domain <strong>{{ new_record_name }}</strong></h2>
<form> <form method="post" action="">
<h3>Mail servers</h3> <h3>Mail servers</h3>
<fieldset> <input name="mailservers[_exist]" type="hidden" value="true">
<legend>Mail server #1</legend>
{% for mailserver in input_data.mailservers.data.mailservers | default(value=[""]) %}
<fieldset>
<legend>Mail server #{{ loop.index }}</legend>
<div class="form-row"> <div class="form-row">
<div class="form-input"> <div class="form-input">
<label for="server">Server name</label> <label for="mailserver-mail_exchanger-{{ loop.index0 }}">Server name</label>
<input name="server" id="server" type="text"> <input
name="mailservers[data][mailservers][{{ loop.index0 }}][mail_exchanger]"
id="mailserver-mail_exchanger-{{ loop.index0 }}"
type="text"
value="{{ mailserver.mail_exchanger | default(value="") }}"
>
</div> </div>
<div class="form-input"> <div class="form-input">
<label for="preference">Preference</label> <label for="mailserver-preference-{{ loop.index0 }}">Preference</label>
<input name="preference" id="preference" type="text"> <input
name="mailservers[data][mailservers][{{ loop.index0 }}][preference]"
id="mailserver-preference-{{ loop.index0 }}"
type="text"
value="{{ mailserver.preference | default(value="") }}"
>
</div> </div>
</div> </div>
</fieldset> </fieldset>
{% endfor %}
<h3>Security</h3> <h3>Security</h3>
<input name="spf[_exist]" type="hidden" value="true">
<div class="form-input"> <div class="form-input">
<label for="spf">Sender policy (SPF)</label> <label for="spf-policy">Sender policy (SPF)</label>
<input name="spf" id="spf" type="text"> <input name="spf[data][policy]" id="spf-policy" type="text" value="{{ input_data.spf.data.policy | default(value="") }}">
</div> </div>
<div class="form-input"> <div class="form-input">