wip: create record form
This commit is contained in:
parent
cfdd9afc0e
commit
08b21ac010
23 changed files with 1541 additions and 322 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -775,6 +775,7 @@ dependencies = [
|
||||||
"rusqlite",
|
"rusqlite",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"serde_urlencoded",
|
||||||
"tera",
|
"tera",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tower",
|
"tower",
|
||||||
|
|
|
@ -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 = "*"
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
250
src/form.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>> {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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()));
|
||||||
|
|
2
src/resources/dns/external/rdata.rs
vendored
2
src/resources/dns/external/rdata.rs
vendored
|
@ -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
|
||||||
|
|
207
src/resources/dns/friendly/base.rs
Normal file
207
src/resources/dns/friendly/base.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
253
src/resources/dns/friendly/create.rs
Normal file
253
src/resources/dns/friendly/create.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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::*;
|
||||||
|
|
|
@ -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 --------- */
|
||||||
|
|
249
src/resources/dns/friendly/record.rs
Normal file
249
src/resources/dns/friendly/record.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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>,
|
||||||
}
|
}
|
||||||
|
|
|
@ -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!()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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">
|
||||||
|
|
Loading…
Reference in a new issue