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",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
"tera",
|
||||
"tokio",
|
||||
"tower",
|
||||
|
|
|
@ -28,3 +28,4 @@ tower-http = { version = "0.6", default-features = false, features = [ "fs" ]}
|
|||
fluent-bundle = "0.15.3"
|
||||
unic-langid = "*"
|
||||
tower = "*"
|
||||
serde_urlencoded = "*"
|
||||
|
|
|
@ -29,3 +29,11 @@ zone-content-record-type-service =
|
|||
zone-content-new-record-button = New record
|
||||
|
||||
## Create record
|
||||
|
||||
record-input-addresses =
|
||||
.input-label = IP address #{ $index }
|
||||
.error-record-parse-ip = Unexpected IP address format. The IP address
|
||||
should be either an IPv4 address, like <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
|
||||
|
||||
## 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::collections::HashMap;
|
||||
|
||||
use axum::http::{self, StatusCode};
|
||||
use axum::response::{AppendHeaders, IntoResponse, Response};
|
||||
|
@ -10,6 +11,7 @@ use serde_json::{Value, json};
|
|||
use crate::dns::DnsDriverError;
|
||||
use crate::resources::dns::external::rdata::RDataValidationError;
|
||||
use crate::resources::dns::external::record::{RecordError, RecordValidationError};
|
||||
use crate::resources::dns::friendly::InputDataError;
|
||||
use crate::resources::zone::ZoneError;
|
||||
use crate::validation::{DomainValidationError, TxtParseError};
|
||||
use crate::template::TemplateError;
|
||||
|
@ -41,6 +43,13 @@ pub fn serialize_status<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 {
|
||||
pub fn new(code: &str, description: &str) -> Self {
|
||||
Error {
|
||||
|
@ -338,6 +347,19 @@ impl From<RDataValidationError> for Error {
|
|||
"input": input
|
||||
}))
|
||||
},
|
||||
RDataValidationError::IpAddress { input } => {
|
||||
Error::new("record:parse:ip", "The IP address {input} is invalid. It should be either an IPv4 address, four numbers between 0 and 255 separated by dots, or and IPv6 address, eight groups of four hexadecimal digit separated by colons.")
|
||||
.with_details(json!({
|
||||
"input": input
|
||||
}))
|
||||
},
|
||||
RDataValidationError::Number { min, max } => {
|
||||
Error::new("record:parse:number", "Expected a number between {min} and {max}")
|
||||
.with_details(json!({
|
||||
"min": min,
|
||||
"max": max,
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -414,3 +436,20 @@ impl From<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))
|
||||
|
||||
}
|
||||
|
||||
fn is_safe(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
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 push_error;
|
||||
pub(crate) use check_type;
|
||||
pub(crate) use get_object_value;
|
||||
|
|
|
@ -8,6 +8,7 @@ mod macros;
|
|||
mod template;
|
||||
mod proto;
|
||||
mod localization;
|
||||
mod form;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
|
@ -66,6 +67,7 @@ async fn main() {
|
|||
/* ----- UI ----- */
|
||||
.route("/zones/{zone_name}/records", routing::get(routes::ui::zones::get_records_page))
|
||||
.route("/zones/{zone_name}/records/new", routing::get(routes::ui::zones::get_new_record_page))
|
||||
.route("/zones/{zone_name}/records/new", routing::post(routes::ui::zones::post_new_record))
|
||||
.nest_service("/assets", ServeDir::new("assets"))
|
||||
.with_state(app_state)
|
||||
.layer(localization.language_middleware("en".parse().unwrap()));
|
||||
|
|
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 {
|
||||
Ip4Address { input: String },
|
||||
Ip6Address { input: String },
|
||||
IpAddress { input: String },
|
||||
Number { min: i128, max: i128 }
|
||||
}
|
||||
|
||||
/// Type used to serialize / deserialize resource records data to response / request
|
||||
|
|
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 record;
|
||||
pub mod base;
|
||||
pub mod create;
|
||||
|
||||
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)]
|
||||
pub enum FriendlyRData {
|
||||
|
@ -48,226 +32,42 @@ pub enum FriendlyRDataAggregated {
|
|||
Texts(Texts),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct FriendlyRecord {
|
||||
ttl: i64,
|
||||
data: FriendlyRDataAggregated,
|
||||
}
|
||||
|
||||
impl Serialize for FriendlyRecord {
|
||||
fn serialize<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 --------- */
|
||||
|
||||
/* --------- Address --------- */
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct Address {
|
||||
pub address: String
|
||||
pub address: IpAddr,
|
||||
}
|
||||
|
||||
impl FromValue for Address {
|
||||
fn from_value(value: serde_json::Value) -> Result<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)]
|
||||
|
@ -275,6 +75,51 @@ pub struct Addresses {
|
|||
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 --------- */
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
|
@ -307,8 +152,45 @@ pub enum ServiceType {
|
|||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct MailServer {
|
||||
pub preference: i64,
|
||||
pub mail_exchanger: String,
|
||||
pub preference: u16,
|
||||
pub mail_exchanger: Name,
|
||||
}
|
||||
|
||||
impl FromValue for MailServer {
|
||||
fn from_value(value: serde_json::Value) -> Result<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)]
|
||||
|
@ -316,6 +198,49 @@ pub struct MailServers {
|
|||
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 --------- */
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
|
@ -344,7 +269,50 @@ pub struct Texts {
|
|||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct Spf {
|
||||
pub policy: String
|
||||
pub policy: Text
|
||||
}
|
||||
|
||||
impl FromValue for Spf {
|
||||
fn from_value(value: serde_json::Value) -> Result<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 --------- */
|
||||
|
|
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 domain::base::scan::Symbol;
|
||||
use serde::{Serialize, Deserialize, Serializer};
|
||||
|
||||
#[derive(Clone, Eq, PartialEq)]
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Deserialize)]
|
||||
pub struct Name {
|
||||
name: String
|
||||
}
|
||||
|
@ -17,6 +19,15 @@ impl Name {
|
|||
}
|
||||
}
|
||||
|
||||
impl Serialize for Name {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(&self.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Name {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.name)
|
||||
|
@ -88,3 +99,42 @@ impl PartialOrd for Rtype {
|
|||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct Text {
|
||||
pub data: Vec<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 super::{Name, Rtype};
|
||||
use super::{Name, Rtype, Text};
|
||||
|
||||
use crate::resources::dns::friendly;
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum RData {
|
||||
A(A),
|
||||
Aaaa(Aaaa),
|
||||
|
@ -45,33 +45,45 @@ impl RData {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct A {
|
||||
pub address: Ipv4Addr,
|
||||
}
|
||||
|
||||
impl A {
|
||||
pub fn new(address: Ipv4Addr) -> Self {
|
||||
A {
|
||||
address
|
||||
}
|
||||
}
|
||||
|
||||
pub fn friendly(self, owner: &str) -> Option<(String, friendly::FriendlyRData)> {
|
||||
Some((owner.to_string(), friendly::FriendlyRData::Address(friendly::Address {
|
||||
address: self.address.to_string()
|
||||
address: self.address.into()
|
||||
})))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Aaaa {
|
||||
pub address: Ipv6Addr,
|
||||
}
|
||||
|
||||
impl Aaaa {
|
||||
|
||||
pub fn new(address: Ipv6Addr) -> Self {
|
||||
Aaaa {
|
||||
address
|
||||
}
|
||||
}
|
||||
pub fn friendly(self, owner: &str) -> Option<(String, friendly::FriendlyRData)> {
|
||||
Some((owner.to_string(), friendly::FriendlyRData::Address(friendly::Address {
|
||||
address: self.address.to_string()
|
||||
address: self.address.into()
|
||||
})))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Cname {
|
||||
pub target: Name,
|
||||
}
|
||||
|
@ -86,22 +98,29 @@ impl Cname {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Mx {
|
||||
pub preference: u16,
|
||||
pub mail_exchanger: Name,
|
||||
}
|
||||
|
||||
impl Mx {
|
||||
pub fn new(mail_exchanger: Name, preference: u16) -> Self {
|
||||
Mx {
|
||||
mail_exchanger,
|
||||
preference,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn friendly(self, owner: &str) -> Option<(String, friendly::FriendlyRData)> {
|
||||
Some((owner.to_string(), friendly::FriendlyRData::MailServer(friendly::MailServer {
|
||||
preference: self.preference.into(),
|
||||
mail_exchanger: self.mail_exchanger.to_string(),
|
||||
preference: self.preference,
|
||||
mail_exchanger: self.mail_exchanger,
|
||||
})))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Ns {
|
||||
pub target: Name,
|
||||
}
|
||||
|
@ -114,12 +133,12 @@ impl Ns {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Ptr {
|
||||
pub target: Name,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Soa {
|
||||
pub primary_server: Name,
|
||||
pub maintainer: Name,
|
||||
|
@ -130,7 +149,7 @@ pub struct Soa {
|
|||
pub serial: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Srv {
|
||||
pub server: Name,
|
||||
pub port: u16,
|
||||
|
@ -167,7 +186,15 @@ impl Srv {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Txt {
|
||||
pub text: Vec<u8>,
|
||||
}
|
||||
|
||||
impl Txt {
|
||||
pub fn new(text: Text) -> Self {
|
||||
Txt {
|
||||
text: text.bytes()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
use super::rdata::RData;
|
||||
use super::Name;
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Record {
|
||||
pub name: Name,
|
||||
pub ttl: u32,
|
||||
pub rdata: RData
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct RecordList {
|
||||
pub records: Vec<Record>,
|
||||
}
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
use axum::extract::{Query, Path, State, OriginalUri};
|
||||
use axum::Extension;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{Value, json};
|
||||
use unic_langid::LanguageIdentifier;
|
||||
|
||||
use crate::validation;
|
||||
use crate::form::Node;
|
||||
use crate::macros::append_errors;
|
||||
use crate::AppState;
|
||||
use crate::errors::Error;
|
||||
use crate::errors::{Error, error_map};
|
||||
use crate::template::Template;
|
||||
use crate::resources::dns::friendly;
|
||||
use crate::resources::dns::friendly::{self, NewRecordQuery, ConfigurationType, FromValue, NewSectionMail, NewSectionWeb, ToInternal};
|
||||
use crate::resources::dns::internal;
|
||||
|
||||
pub async fn get_records_page(
|
||||
Path(zone_name): Path<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(
|
||||
Path(zone_name): Path<String>,
|
||||
State(app): State<AppState>,
|
||||
Query(params): Query<NewRecordQuery>,
|
||||
Query(mut params): Query<NewRecordQuery>,
|
||||
OriginalUri(url): OriginalUri,
|
||||
Extension(lang): Extension<LanguageIdentifier>,
|
||||
) -> Result<Template<'static, Value>, Error> {
|
||||
let zone = app.db.get_zone_by_name(&zone_name).await?;
|
||||
|
||||
// Syntax validation of the subdomain
|
||||
let (name, domain_error) = if let Some(input_name) = params.subdomain {
|
||||
if input_name.is_empty() {
|
||||
(Some(zone.name.clone()), None)
|
||||
} else {
|
||||
let new_name = format!("{input_name}.{zone_name}");
|
||||
let res = validation::normalize_domain(&new_name);
|
||||
match res {
|
||||
Err(error) => (Some(input_name), Some(error)),
|
||||
Ok(new_name) => (Some(new_name), None),
|
||||
}
|
||||
}
|
||||
} else {
|
||||
(None, None)
|
||||
};
|
||||
|
||||
println!("{}", lang);
|
||||
let mut errors = Vec::new();
|
||||
append_errors!(params.validate(&zone.name), errors);
|
||||
|
||||
Ok(Template::new(
|
||||
"pages/new_record.html",
|
||||
app.template_engine,
|
||||
json!({
|
||||
"current_zone": zone.name,
|
||||
"new_record_name": name,
|
||||
"domain_error": domain_error,
|
||||
"new_record_name": params.name,
|
||||
"errors": error_map(errors),
|
||||
"config": params.config,
|
||||
"rtype": params.rtype,
|
||||
"url": url.to_string(),
|
||||
|
@ -87,3 +60,70 @@ pub async fn get_new_record_page(
|
|||
})
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn post_new_record(
|
||||
Path(zone_name): Path<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);
|
||||
if digits.len() < 3 {
|
||||
errors.push(Error::from(TxtParseError::BadEscapeDigitsTooShort { sequence: String::from_iter(digits) }))
|
||||
} else if digits.iter().all(|c| c.is_ascii_digit()) {
|
||||
} else if !digits.iter().all(|c| c.is_ascii_digit()) {
|
||||
errors.push(Error::from(TxtParseError::BadEscapeDigitsNotDigits { sequence: String::from_iter(digits) }))
|
||||
} else {
|
||||
let index = {
|
||||
|
@ -91,6 +91,8 @@ pub fn parse_txt_data(text: &str) -> Result<Vec<u8>, Vec<Error>> {
|
|||
if index > 255 {
|
||||
errors.push(Error::from(TxtParseError::BadEscapeDigitIndexTooHigh { sequence: String::from_iter(digits) }))
|
||||
}
|
||||
|
||||
data.push(index as u8)
|
||||
}
|
||||
} else if printable(ch) {
|
||||
data.push(ch as u8)
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
{% block main %}
|
||||
<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) %}
|
||||
{% include "pages/new_record/choose_name.html" %}
|
||||
{% elif not config and not rtype %}
|
||||
|
|
|
@ -1,19 +1,21 @@
|
|||
<h2>Choose the name of the new record</h2>
|
||||
<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">
|
||||
<input
|
||||
type="text"
|
||||
name="subdomain"
|
||||
id="subdomain"
|
||||
name="name"
|
||||
id="name"
|
||||
{% 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>
|
||||
</div>
|
||||
{% if domain_error %}
|
||||
<p class="error" id="subdomain-error">{{ domain_error.description }}</p>
|
||||
<p class="error" id="name-error">{{ domain_error.description }}</p>
|
||||
{% 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>
|
||||
</form>
|
||||
|
|
|
@ -2,43 +2,84 @@
|
|||
|
||||
<h2>Configure a web site for the domain <strong>{{ new_record_name }}</strong></h2>
|
||||
|
||||
<form>
|
||||
<form method="post" action="">
|
||||
<h3>Web servers</h3>
|
||||
|
||||
<input name="addresses[_exist]" type="hidden" value="true">
|
||||
|
||||
<div class="form-input">
|
||||
<label for="record-0-addresses-address-0">IP Address #1</label>
|
||||
<input name="records[0][addresses][addresses][0][address]" id="record-0-addresses-address-0" type="text">
|
||||
<label for="addresses-ttl">Duration in cache (TTL) (optional, default to 1 hour)</label>
|
||||
<input id="addresses-ttl" name="addresses[ttl]" type="text">
|
||||
</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>
|
||||
|
||||
{% elif config == "mail" %}
|
||||
|
||||
<h2>Configure e-mails for the domain <strong>{{ new_record_name }}</strong></h2>
|
||||
|
||||
<form>
|
||||
<form method="post" action="">
|
||||
<h3>Mail servers</h3>
|
||||
<fieldset>
|
||||
<legend>Mail server #1</legend>
|
||||
<input name="mailservers[_exist]" type="hidden" value="true">
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-input">
|
||||
<label for="server">Server name</label>
|
||||
<input name="server" id="server" type="text">
|
||||
</div>
|
||||
{% for mailserver in input_data.mailservers.data.mailservers | default(value=[""]) %}
|
||||
<fieldset>
|
||||
<legend>Mail server #{{ loop.index }}</legend>
|
||||
<div class="form-row">
|
||||
<div class="form-input">
|
||||
<label for="mailserver-mail_exchanger-{{ loop.index0 }}">Server name</label>
|
||||
<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 class="form-input">
|
||||
<label for="preference">Preference</label>
|
||||
<input name="preference" id="preference" type="text">
|
||||
<div class="form-input">
|
||||
<label for="mailserver-preference-{{ loop.index0 }}">Preference</label>
|
||||
<input
|
||||
name="mailservers[data][mailservers][{{ loop.index0 }}][preference]"
|
||||
id="mailserver-preference-{{ loop.index0 }}"
|
||||
type="text"
|
||||
value="{{ mailserver.preference | default(value="") }}"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</fieldset>
|
||||
{% endfor %}
|
||||
|
||||
<h3>Security</h3>
|
||||
<input name="spf[_exist]" type="hidden" value="true">
|
||||
|
||||
<div class="form-input">
|
||||
<label for="spf">Sender policy (SPF)</label>
|
||||
<input name="spf" id="spf" type="text">
|
||||
<label for="spf-policy">Sender policy (SPF)</label>
|
||||
<input name="spf[data][policy]" id="spf-policy" type="text" value="{{ input_data.spf.data.policy | default(value="") }}">
|
||||
</div>
|
||||
|
||||
<div class="form-input">
|
||||
|
|
Loading…
Reference in a new issue