wip: friendly records
This commit is contained in:
parent
7e3e927946
commit
7cee790c85
14 changed files with 477 additions and 149 deletions
9
Cargo.lock
generated
9
Cargo.lock
generated
|
@ -68,12 +68,13 @@ checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "axum"
|
name = "axum"
|
||||||
version = "0.8.0-rc.1"
|
version = "0.8.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "47b212eb691030da38f1eff381777e431eb6f0760a0d02ffcb1702f1da9894e2"
|
checksum = "6d6fd624c75e18b3b4c6b9caf42b1afe24437daaee904069137d8bab077be8b8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"axum-core",
|
"axum-core",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
"form_urlencoded",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"http",
|
"http",
|
||||||
"http-body",
|
"http-body",
|
||||||
|
@ -100,9 +101,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "axum-core"
|
name = "axum-core"
|
||||||
version = "0.5.0-rc.1"
|
version = "0.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "30256e79153b08607dcf9e0a72bd31564bc9228b9f145d1e1a29e3d01ad1fd16"
|
checksum = "df1362f362fd16024ae199c1970ce98f9661bf5ef94b9808fee734bc3698b733"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
name = "nomilo"
|
name = "nomilo"
|
||||||
version = "0.2.0-dev"
|
version = "0.2.0-dev"
|
||||||
authors = ["DNS Witch Collective <dns-witch@dns-witch.eu.org>"]
|
authors = ["DNS Witch Collective <dns-witch@dns-witch.eu.org>"]
|
||||||
edition = "2021"
|
edition = "2024"
|
||||||
license = "AGPL-3.0-or-later"
|
license = "AGPL-3.0-or-later"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
repository = "https://git.familier.net.eu.org/dns-witch/nomilo"
|
repository = "https://git.familier.net.eu.org/dns-witch/nomilo"
|
||||||
|
@ -20,7 +20,7 @@ tokio = {version = "1", default-features = false, features = [ "macros", "rt-mul
|
||||||
#rand = "0.8"
|
#rand = "0.8"
|
||||||
tera = { version = "1", default-features = false }
|
tera = { version = "1", default-features = false }
|
||||||
domain = { version = "0.10.3", features = [ "tsig", "unstable-client-transport" ]}
|
domain = { version = "0.10.3", features = [ "tsig", "unstable-client-transport" ]}
|
||||||
axum = { version = "0.8.0-rc.1", default-features = false, features = [ "http1", "json", "form", "query", "tokio" ]}
|
axum = { version = "0.8.1", default-features = false, features = [ "http1", "json", "form", "query", "tokio" ]}
|
||||||
bb8 = { version = "0.9" }
|
bb8 = { version = "0.9" }
|
||||||
rusqlite = { version = "0.32"}
|
rusqlite = { version = "0.32"}
|
||||||
async-trait = { version = "0.1" }
|
async-trait = { version = "0.1" }
|
||||||
|
|
|
@ -23,6 +23,14 @@ main {
|
||||||
margin: auto;
|
margin: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-weight: 200;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
|
||||||
article.domain {
|
article.domain {
|
||||||
margin-bottom: 2em;
|
margin-bottom: 2em;
|
||||||
}
|
}
|
||||||
|
@ -38,7 +46,7 @@ article.domain header h3.folder-tab {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 0 1em;
|
padding: 0 1rem;
|
||||||
border-top-left-radius: .3rem;
|
border-top-left-radius: .3rem;
|
||||||
background-color: #f2e0fd;
|
background-color: #f2e0fd;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
@ -52,26 +60,23 @@ article.domain header h3.folder-tab ~ .sep {
|
||||||
background-color: #f2e0fd;
|
background-color: #f2e0fd;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
clip-path: url("#corner-folder-tab-right");
|
clip-path: url("#corner-folder-tab-right");
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
article.domain .records {
|
||||||
|
background: #f2e0fd;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 0 .3rem .3rem .3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
article.domain .records h4 {
|
||||||
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
article.domain .records > ul {
|
article.domain .records > ul {
|
||||||
background: #f2e0fd;
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
padding: 1rem;;
|
|
||||||
border-radius: 0 .3rem .3rem .3rem;
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: auto 1fr;
|
|
||||||
row-gap: 1rem;
|
|
||||||
column-gap: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
article.domain .records .rrset {
|
|
||||||
display: grid;
|
|
||||||
align-items: baseline;
|
|
||||||
grid-template-columns: subgrid;
|
|
||||||
grid-column: 1 / 3;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
article.domain .records .rrset .rtype {
|
article.domain .records .rrset .rtype {
|
||||||
|
@ -80,18 +85,8 @@ article.domain .records .rrset .rtype {
|
||||||
gap: .5em;
|
gap: .5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
article.domain .records .rrset .rtype::after {
|
|
||||||
content: '';
|
|
||||||
display: block;
|
|
||||||
flex: 1;
|
|
||||||
border-bottom: .2rem solid #850085;
|
|
||||||
padding-left: 1em;
|
|
||||||
position: relative;
|
|
||||||
bottom: .25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
article.domain .records .rrset ul {
|
article.domain .records .rrset ul {
|
||||||
padding: 0;
|
padding: 1rem 0 1rem 2rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: .5rem;
|
gap: .5rem;
|
||||||
|
@ -105,32 +100,16 @@ article.domain .records .rrset li {
|
||||||
gap: .5rem;
|
gap: .5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
article.domain .records .rrset li::before {
|
article.domain .records .rrset .rdata {
|
||||||
content: '';
|
display: flex;
|
||||||
height: 1em;
|
gap: .2rem;
|
||||||
width: 1rem;
|
flex-wrap: wrap;
|
||||||
border-bottom: .2rem solid #850085;
|
|
||||||
position: relative;
|
|
||||||
bottom: .25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
article.domain .records .rrset li:not(:first-child)::before {
|
|
||||||
border-left: .2rem solid #850085;
|
|
||||||
border-bottom-left-radius: .3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
article.domain .records .rrset li:not(:last-child)::after {
|
|
||||||
content: '';
|
|
||||||
height: 100%;
|
|
||||||
width: 1rem;
|
|
||||||
position: absolute;
|
|
||||||
top: 1.1em;
|
|
||||||
border-left: .2rem solid #850085;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
article.domain .records .rrset .rdata-main {
|
article.domain .records .rrset .rdata-main {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: .3rem;
|
gap: .3rem;
|
||||||
|
margin-right: .1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
article.domain .records .rrset .rdata-main .pill {
|
article.domain .records .rrset .rdata-main .pill {
|
||||||
|
@ -138,7 +117,6 @@ article.domain .records .rrset .rdata-main .pill {
|
||||||
}
|
}
|
||||||
|
|
||||||
article.domain .records .rrset .rdata-complementary {
|
article.domain .records .rrset .rdata-complementary {
|
||||||
margin-top: .2em;
|
|
||||||
font-size: .9em;
|
font-size: .9em;
|
||||||
gap: .2rem;
|
gap: .2rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
@ -6,6 +6,12 @@ example.com. IN SOA ns.example.com. admin.example.com. (
|
||||||
300 ; minimum (5 minutes)
|
300 ; minimum (5 minutes)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
example.com. 600 IN A 198.51.100.3
|
||||||
|
example.com. 600 IN AAAA 2001:db8:cafe:bc68::2
|
||||||
|
|
||||||
|
example.com. 3600 IN MX 1 srv1.example.com.
|
||||||
|
example.com. 3600 IN MX 100 mail.example.net.
|
||||||
|
|
||||||
example.com. 84600 IN NS ns.example.com.
|
example.com. 84600 IN NS ns.example.com.
|
||||||
ns.example.com. 84600 IN A 198.51.100.3
|
ns.example.com. 84600 IN A 198.51.100.3
|
||||||
|
|
||||||
|
|
|
@ -58,7 +58,8 @@ async fn main() {
|
||||||
.route("/api/zones/{zone_name}/records", routing::get(routes::api::zones::get_zone_records))
|
.route("/api/zones/{zone_name}/records", routing::get(routes::api::zones::get_zone_records))
|
||||||
.route("/api/zones/{zone_name}/records", routing::post(routes::api::zones::create_zone_records))
|
.route("/api/zones/{zone_name}/records", routing::post(routes::api::zones::create_zone_records))
|
||||||
/* ----- UI ----- */
|
/* ----- UI ----- */
|
||||||
.route("/zones/{zone_name}/records", routing::get(routes::ui::zones::get_zone_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))
|
||||||
.nest_service("/assets", ServeDir::new("assets"))
|
.nest_service("/assets", ServeDir::new("assets"))
|
||||||
.with_state(app_state);
|
.with_state(app_state);
|
||||||
|
|
||||||
|
|
|
@ -461,22 +461,3 @@ impl TryFrom<internal::Record> for RecordImpl {
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
pub struct RecordList {
|
|
||||||
pub records: Vec<RecordImpl>
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryFrom<internal::RecordList> for RecordList {
|
|
||||||
type Error = Error;
|
|
||||||
|
|
||||||
fn try_from(value: internal::RecordList) -> Result<Self, Self::Error> {
|
|
||||||
let mut records = Vec::with_capacity(value.records.len());
|
|
||||||
|
|
||||||
for record in value.records.into_iter() {
|
|
||||||
records.push(record.try_into()?)
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(RecordList { records })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1 +1,3 @@
|
||||||
pub mod rdata;
|
pub mod rdata;
|
||||||
|
|
||||||
|
pub use rdata::*;
|
||||||
|
|
|
@ -1,98 +1,309 @@
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize, Serializer, ser::SerializeStruct};
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
use crate::resources::dns::internal;
|
||||||
pub enum FriendlyRData {
|
|
||||||
Address(Address),
|
|
||||||
Service(Service),
|
|
||||||
MailServer(MailServer), // Include SPF, etc ?!
|
|
||||||
NameServer(NameServer),
|
|
||||||
TextData(TextData),
|
|
||||||
Alias(Alias),
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize, PartialEq)]
|
||||||
|
#[serde(rename_all="lowercase")]
|
||||||
pub enum FriendlyRType {
|
pub enum FriendlyRType {
|
||||||
Address,
|
Address,
|
||||||
Service,
|
Alias,
|
||||||
MailServer,
|
MailServer,
|
||||||
NameServer,
|
NameServer,
|
||||||
|
Service,
|
||||||
|
Spf,
|
||||||
TextData,
|
TextData,
|
||||||
Alias
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct FriendlyRecord {
|
impl fmt::Display for FriendlyRType {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
FriendlyRType::Address => write!(f, "address"),
|
||||||
|
FriendlyRType::Alias => write!(f, "alias"),
|
||||||
|
FriendlyRType::MailServer => write!(f, "mailserver"),
|
||||||
|
FriendlyRType::NameServer => write!(f, "nameserver"),
|
||||||
|
FriendlyRType::Service => write!(f, "service"),
|
||||||
|
FriendlyRType::Spf => write!(f, "spf"),
|
||||||
|
FriendlyRType::TextData => write!(f, "textdata"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait HasRType {
|
||||||
|
fn rtype(&self) -> FriendlyRType;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
|
#[serde(tag = "_type")]
|
||||||
|
pub enum FriendlyRData {
|
||||||
|
Address(Address),
|
||||||
|
Alias(Alias),
|
||||||
|
MailServer(MailServer),
|
||||||
|
NameServer(NameServer),
|
||||||
|
Service(Service),
|
||||||
|
Spf(Spf),
|
||||||
|
TextData(TextData),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HasRType for FriendlyRData {
|
||||||
|
fn rtype(&self) -> FriendlyRType {
|
||||||
|
match self {
|
||||||
|
FriendlyRData::Address(_) => FriendlyRType::Address,
|
||||||
|
FriendlyRData::Alias(_) => FriendlyRType::Alias,
|
||||||
|
FriendlyRData::MailServer(_) => FriendlyRType::MailServer,
|
||||||
|
FriendlyRData::NameServer(_) => FriendlyRType::NameServer,
|
||||||
|
FriendlyRData::Service(_) => FriendlyRType::Service,
|
||||||
|
FriendlyRData::Spf(_) => FriendlyRType::Spf,
|
||||||
|
FriendlyRData::TextData(_) => FriendlyRType::TextData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct FriendlyRecord<T> {
|
||||||
|
ttl: i64,
|
||||||
|
data: T,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Serialize + HasRType> Serialize for FriendlyRecord<T> {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: Serializer,
|
||||||
|
{
|
||||||
|
let mut state = serializer.serialize_struct("FriendlyRecord", 3)?;
|
||||||
|
state.serialize_field("ttl", &self.ttl)?;
|
||||||
|
state.serialize_field("data", &self.data)?;
|
||||||
|
state.serialize_field("rtype", &self.data.rtype())?;
|
||||||
|
state.end()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> FriendlyRecord<T> {
|
||||||
|
fn new(ttl: u32, data: T) -> Self {
|
||||||
|
FriendlyRecord {
|
||||||
|
ttl: ttl.into(),
|
||||||
|
data,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct FriendlyRecordSet<T> {
|
||||||
|
ttl: i64,
|
||||||
|
data: Vec<T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Serialize + HasRType> Serialize for FriendlyRecordSet<T> {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: Serializer,
|
||||||
|
{
|
||||||
|
let mut state = serializer.serialize_struct("FriendlyRecordSet", 3)?;
|
||||||
|
state.serialize_field("ttl", &self.ttl)?;
|
||||||
|
state.serialize_field("data", &self.data)?;
|
||||||
|
state.serialize_field("rtype", &self.data.first().map(|d| d.rtype().to_string()))?;
|
||||||
|
state.end()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> FriendlyRecordSet<T> {
|
||||||
|
fn new(ttl: u32) -> Self {
|
||||||
|
FriendlyRecordSet {
|
||||||
|
ttl: ttl.into(),
|
||||||
|
data: Vec::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
|
#[serde(rename_all="lowercase")]
|
||||||
|
pub enum ConfigurationType {
|
||||||
|
Mail,
|
||||||
|
Web,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub struct MailConfiguration {
|
||||||
|
pub servers: Option<FriendlyRecordSet<MailServer>>,
|
||||||
|
pub spf: Option<FriendlyRecord<Spf>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MailConfiguration {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
MailConfiguration {
|
||||||
|
servers: None,
|
||||||
|
spf: None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub struct WebConfiguration {
|
||||||
|
pub addresses: Option<FriendlyRecordSet<Address>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
impl WebConfiguration {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
WebConfiguration {
|
||||||
|
addresses: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub struct FriendlyRecordSetGroup {
|
||||||
owner: String,
|
owner: String,
|
||||||
friendly_type: FriendlyRType,
|
general_records: Vec<FriendlyRecordSet<FriendlyRData>>,
|
||||||
rdata: FriendlyRData,
|
mail: Option<MailConfiguration>,
|
||||||
ttl: u32,
|
web: Option<WebConfiguration>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct RecordSet {
|
impl FriendlyRecordSetGroup {
|
||||||
friendly_type: FriendlyRType,
|
pub fn new(owner: String) -> Self {
|
||||||
records: Vec<FriendlyRecord>
|
FriendlyRecordSetGroup {
|
||||||
|
owner,
|
||||||
|
general_records: Vec::new(),
|
||||||
|
mail: None,
|
||||||
|
web: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct RecordSetGroup {
|
#[derive(Debug, Serialize)]
|
||||||
owner: String,
|
pub struct FriendlyRecords {
|
||||||
rrsets: Vec<RecordSet>
|
records: Vec<FriendlyRecordSetGroup>,
|
||||||
|
aliases: Vec<Alias>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct FriendlyRecords(Vec<RecordSetGroup>);
|
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();
|
||||||
|
|
||||||
|
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 rdata = rdata.unwrap();
|
||||||
|
|
||||||
|
if let FriendlyRData::Alias(alias) = rdata {
|
||||||
|
records.aliases.push(alias)
|
||||||
|
} else {
|
||||||
|
|
||||||
|
if records.records.is_empty() {
|
||||||
|
records.records.push(FriendlyRecordSetGroup::new(name));
|
||||||
|
} else {
|
||||||
|
let group = records.records.last().unwrap();
|
||||||
|
|
||||||
|
if group.owner != name {
|
||||||
|
records.records.push(FriendlyRecordSetGroup::new(name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let current_group = records.records.last_mut().unwrap();
|
||||||
|
|
||||||
|
match rdata {
|
||||||
|
FriendlyRData::Address(address) => {
|
||||||
|
let web = current_group.web.get_or_insert_with(WebConfiguration::new);
|
||||||
|
let addresses = web.addresses.get_or_insert_with(|| FriendlyRecordSet::new(ttl));
|
||||||
|
|
||||||
|
addresses.data.push(address);
|
||||||
|
},
|
||||||
|
FriendlyRData::MailServer(mailserver) => {
|
||||||
|
let mail: &mut MailConfiguration = current_group.mail.get_or_insert_with(MailConfiguration::new);
|
||||||
|
let servers = mail.servers.get_or_insert_with(|| FriendlyRecordSet::new(ttl));
|
||||||
|
|
||||||
|
servers.data.push(mailserver);
|
||||||
|
},
|
||||||
|
FriendlyRData::Spf(spf) => {
|
||||||
|
let mail: &mut MailConfiguration = current_group.mail.get_or_insert_with(MailConfiguration::new);
|
||||||
|
|
||||||
|
mail.spf = Some(FriendlyRecord::new(ttl, spf));
|
||||||
|
},
|
||||||
|
FriendlyRData::Alias(_) => {},
|
||||||
|
data => {
|
||||||
|
// TODO: NS -> Skip if NS for zone (authority), create Delegation section with glue + DS for others (how to check if record is glue?)
|
||||||
|
|
||||||
|
if current_group.general_records.is_empty() {
|
||||||
|
current_group.general_records.push(FriendlyRecordSet::new(ttl));
|
||||||
|
} else {
|
||||||
|
let rrset = current_group.general_records.last().unwrap();
|
||||||
|
if rrset.data.last().unwrap().rtype() != data.rtype() {
|
||||||
|
current_group.general_records.push(FriendlyRecordSet::new(ttl));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let current_rrset = current_group.general_records.last_mut().unwrap();
|
||||||
|
current_rrset.data.push(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
records
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --------- RDATA --------- */
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
pub struct Address {
|
pub struct Address {
|
||||||
pub address: String
|
pub address: String
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
impl HasRType for Address {
|
||||||
|
fn rtype(&self) -> FriendlyRType {
|
||||||
|
FriendlyRType::Address
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
pub struct Service {
|
pub struct Service {
|
||||||
pub service_type: String,
|
pub service_type: ServiceType,
|
||||||
pub port: u16,
|
pub service_name: Option<String>,
|
||||||
pub weight: u16,
|
pub service_protocol: Option<String>,
|
||||||
pub priority: u16,
|
pub port: i64,
|
||||||
|
pub weight: i64,
|
||||||
|
pub priority: i64,
|
||||||
pub server: String,
|
pub server: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
pub enum ServiceType {
|
pub enum ServiceType {
|
||||||
ServiceProtocol { service_name: String, protocol: String },
|
Other,
|
||||||
Service { service_name: String },
|
|
||||||
None,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ServiceType {
|
|
||||||
pub fn from_name(name: String) -> (Self, String) {
|
|
||||||
let labels: Vec<_> = name.splitn(3, '.').collect();
|
|
||||||
let prefix: Vec<_> = labels.iter().cloned().take(2).map(|label| label.strip_prefix('_')).collect();
|
|
||||||
|
|
||||||
if prefix.is_empty() || prefix[0].is_none() {
|
|
||||||
(ServiceType::None, labels[0..].join(".").to_owned())
|
|
||||||
} else if prefix.len() == 1 || prefix[1].is_none() {
|
|
||||||
(ServiceType::Service { service_name: prefix[0].unwrap().into() }, labels[1..].join(".").to_owned())
|
|
||||||
} else {
|
|
||||||
(ServiceType::ServiceProtocol { service_name: prefix[0].unwrap().into(), protocol: prefix[1].unwrap().into() }, labels[2].to_owned())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for ServiceType {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
match self {
|
|
||||||
ServiceType::ServiceProtocol { service_name, protocol } => write!(f, "{}/{}", service_name, protocol),
|
|
||||||
ServiceType::Service { service_name, } => write!(f, "{}", service_name),
|
|
||||||
ServiceType::None => write!(f, "-"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
pub struct MailServer {
|
pub struct MailServer {
|
||||||
pub preference: u16,
|
pub preference: i64,
|
||||||
pub mail_exchanger: String,
|
pub mail_exchanger: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl HasRType for MailServer {
|
||||||
|
fn rtype(&self) -> FriendlyRType {
|
||||||
|
FriendlyRType::MailServer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
pub struct NameServer {
|
pub struct NameServer {
|
||||||
pub target: String,
|
pub target: String,
|
||||||
|
@ -105,5 +316,18 @@ pub struct TextData {
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
pub struct Alias {
|
pub struct Alias {
|
||||||
|
pub from: String,
|
||||||
pub target: String,
|
pub target: String,
|
||||||
|
pub ttl: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
|
pub struct Spf {
|
||||||
|
pub policy: String
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HasRType for Spf {
|
||||||
|
fn rtype(&self) -> FriendlyRType {
|
||||||
|
FriendlyRType::Spf
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,8 @@ use std::net::{Ipv4Addr, Ipv6Addr};
|
||||||
|
|
||||||
use super::{Name, Rtype};
|
use super::{Name, Rtype};
|
||||||
|
|
||||||
|
use crate::resources::dns::friendly;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub enum RData {
|
pub enum RData {
|
||||||
A(A),
|
A(A),
|
||||||
|
@ -29,6 +31,18 @@ impl RData {
|
||||||
RData::Txt(_) => Rtype::Txt,
|
RData::Txt(_) => Rtype::Txt,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn friendly(self, owner: &str, ttl: u32) -> Option<friendly::FriendlyRData> {
|
||||||
|
match self {
|
||||||
|
RData::A(data) => data.friendly(),
|
||||||
|
RData::Aaaa(data) => data.friendly(),
|
||||||
|
RData::Cname(data) => data.friendly(owner, ttl),
|
||||||
|
RData::Mx(data) => data.friendly(),
|
||||||
|
RData::Ns(data) => data.friendly(),
|
||||||
|
RData::Srv(data) => data.friendly(owner),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
|
@ -36,27 +50,70 @@ pub struct A {
|
||||||
pub address: Ipv4Addr,
|
pub address: Ipv4Addr,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl A {
|
||||||
|
pub fn friendly(self) -> Option<friendly::FriendlyRData> {
|
||||||
|
Some(friendly::FriendlyRData::Address(friendly::Address {
|
||||||
|
address: self.address.to_string()
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Aaaa {
|
pub struct Aaaa {
|
||||||
pub address: Ipv6Addr,
|
pub address: Ipv6Addr,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Aaaa {
|
||||||
|
pub fn friendly(self) -> Option<friendly::FriendlyRData> {
|
||||||
|
Some(friendly::FriendlyRData::Address(friendly::Address {
|
||||||
|
address: self.address.to_string()
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Cname {
|
pub struct Cname {
|
||||||
pub target: Name,
|
pub target: Name,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Cname {
|
||||||
|
pub fn friendly(self, owner: &str, ttl: u32) -> Option<friendly::FriendlyRData> {
|
||||||
|
Some(friendly::FriendlyRData::Alias(friendly::Alias {
|
||||||
|
from: owner.into(),
|
||||||
|
target: self.target.to_string(),
|
||||||
|
ttl: ttl.into(),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Mx {
|
pub struct Mx {
|
||||||
pub preference: u16,
|
pub preference: u16,
|
||||||
pub mail_exchanger: Name,
|
pub mail_exchanger: Name,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Mx {
|
||||||
|
pub fn friendly(self) -> Option<friendly::FriendlyRData> {
|
||||||
|
Some(friendly::FriendlyRData::MailServer(friendly::MailServer {
|
||||||
|
preference: self.preference.into(),
|
||||||
|
mail_exchanger: self.mail_exchanger.to_string(),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Ns {
|
pub struct Ns {
|
||||||
pub target: Name,
|
pub target: Name,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Ns {
|
||||||
|
pub fn friendly(self) -> Option<friendly::FriendlyRData> {
|
||||||
|
Some(friendly::FriendlyRData::NameServer(friendly::NameServer {
|
||||||
|
target: self.target.to_string(),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Ptr {
|
pub struct Ptr {
|
||||||
pub target: Name,
|
pub target: Name,
|
||||||
|
@ -81,6 +138,32 @@ pub struct Srv {
|
||||||
pub weight: u16,
|
pub weight: u16,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Srv {
|
||||||
|
pub fn friendly(self, owner: &str) -> Option<friendly::FriendlyRData> {
|
||||||
|
let labels: Vec<_> = owner.splitn(3, '.').collect();
|
||||||
|
if labels.len() != 3 {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
let service_name = labels[0]. strip_prefix('_');
|
||||||
|
let protocol = labels[1]. strip_prefix('_');
|
||||||
|
if let (Some(service_name), Some(protocol)) = (service_name, protocol) {
|
||||||
|
Some(friendly::FriendlyRData::Service(friendly::Service {
|
||||||
|
service_type: friendly::ServiceType::Other,
|
||||||
|
service_name: Some(service_name.into()),
|
||||||
|
service_protocol: Some(protocol.into()),
|
||||||
|
port: self.port.into(),
|
||||||
|
weight: self.weight.into(),
|
||||||
|
priority: self.priority.into(),
|
||||||
|
server: self.server.to_string(),
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Txt {
|
pub struct Txt {
|
||||||
pub text: Vec<u8>,
|
pub text: Vec<u8>,
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
pub mod external;
|
pub mod external;
|
||||||
pub mod internal;
|
pub mod internal;
|
||||||
//pub mod friendly;
|
pub mod friendly;
|
||||||
|
|
|
@ -41,9 +41,8 @@ impl Zone {
|
||||||
db.create_zone(create_zone).await
|
db.create_zone(create_zone).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_records(zone_name: &str, db: BoxedDb, record_driver: BoxedRecordDriver) ->Result<RecordList, Error> {
|
pub async fn get_records(&self, record_driver: BoxedRecordDriver) ->Result<RecordList, Error> {
|
||||||
let zone = db.get_zone_by_name(zone_name).await?;
|
let mut records = record_driver.get_records(&self.name).await?;
|
||||||
let mut records = record_driver.get_records(&zone.name).await?;
|
|
||||||
|
|
||||||
records.sort();
|
records.sort();
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,8 @@ pub async fn get_zone_records(
|
||||||
State(app): State<AppState>,
|
State(app): State<AppState>,
|
||||||
) -> Result<Json<external::RecordList>, Error>
|
) -> Result<Json<external::RecordList>, Error>
|
||||||
{
|
{
|
||||||
Zone::get_records(&zone_name, app.db, app.records)
|
let zone = app.db.get_zone_by_name(&zone_name).await?;
|
||||||
|
zone.get_records(app.records)
|
||||||
.await
|
.await
|
||||||
.map(|records| Json(records.into()))
|
.map(|records| Json(records.into()))
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,26 +1,75 @@
|
||||||
use axum::extract::{Path, State};
|
use axum::extract::Request;
|
||||||
|
use axum::extract::{Query, Path, State};
|
||||||
|
use serde::Deserialize;
|
||||||
use serde_json::{Value, json};
|
use serde_json::{Value, json};
|
||||||
|
|
||||||
|
use crate::validation;
|
||||||
use crate::AppState;
|
use crate::AppState;
|
||||||
use crate::errors::Error;
|
use crate::errors::Error;
|
||||||
use crate::resources::zone::Zone;
|
|
||||||
use crate::template::Template;
|
use crate::template::Template;
|
||||||
use crate::resources::dns::external;
|
use crate::resources::dns::friendly;
|
||||||
|
|
||||||
pub async fn get_zone_records_page(
|
pub async fn get_records_page(
|
||||||
Path(zone_name): Path<String>,
|
Path(zone_name): Path<String>,
|
||||||
State(app): State<AppState>,
|
State(app): State<AppState>,
|
||||||
|
request: Request,
|
||||||
) -> Result<Template<'static, Value>, Error> {
|
) -> Result<Template<'static, Value>, Error> {
|
||||||
let records = Zone::get_records(&zone_name, app.db, app.records).await?;
|
let zone = app.db.get_zone_by_name(&zone_name).await?;
|
||||||
|
let records = zone.get_records(app.records).await?;
|
||||||
//records.0.sort_by_key(|record| (&record.name, record.rdata));
|
let records = friendly::FriendlyRecords::from(records.clone());
|
||||||
|
|
||||||
Ok(Template::new(
|
Ok(Template::new(
|
||||||
"pages/records.html",
|
"pages/records.html",
|
||||||
app.template_engine,
|
app.template_engine,
|
||||||
json!({
|
json!({
|
||||||
"current_zone": zone_name,
|
"current_zone": zone.name,
|
||||||
"records": external::RecordList::from(records),
|
"records": records,
|
||||||
|
"url": request.uri().to_string(),
|
||||||
|
})
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct NewRecordQuery {
|
||||||
|
subdomain: Option<String>,
|
||||||
|
config: Option<friendly::ConfigurationType>,
|
||||||
|
rtype: Option<friendly::FriendlyRType>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_new_record_page(
|
||||||
|
Path(zone_name): Path<String>,
|
||||||
|
State(app): State<AppState>,
|
||||||
|
Query(params): Query<NewRecordQuery>,
|
||||||
|
request: Request,
|
||||||
|
) -> 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)
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Template::new(
|
||||||
|
"pages/new_record.html",
|
||||||
|
app.template_engine,
|
||||||
|
json!({
|
||||||
|
"current_zone": zone.name,
|
||||||
|
"new_record_name": name,
|
||||||
|
"domain_error": domain_error,
|
||||||
|
"config": params.config,
|
||||||
|
"rtype": params.rtype,
|
||||||
|
"url": request.uri().to_string(),
|
||||||
})
|
})
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,11 +13,13 @@ pub enum DomainValidationError {
|
||||||
/// https://doc.zonemaster.fr/v2024.1/specifications/tests/RequirementsAndNormalizationOfDomainNames.html
|
/// https://doc.zonemaster.fr/v2024.1/specifications/tests/RequirementsAndNormalizationOfDomainNames.html
|
||||||
/// TODO: No support of dots in labels, how to handle RNAME in SOA?
|
/// TODO: No support of dots in labels, how to handle RNAME in SOA?
|
||||||
pub fn normalize_domain(domain_name: &str) -> Result<String, Error> {
|
pub fn normalize_domain(domain_name: &str) -> Result<String, Error> {
|
||||||
let domain = domain_name.strip_prefix('.').unwrap_or(domain_name).to_lowercase();
|
if domain_name.is_empty() {
|
||||||
|
return Err(Error::from(DomainValidationError::EmptyDomain))
|
||||||
|
}
|
||||||
|
|
||||||
if domain.is_empty() {
|
let domain = domain_name.strip_suffix('.').unwrap_or(domain_name).to_lowercase();
|
||||||
Err(Error::from(DomainValidationError::EmptyDomain))
|
|
||||||
} else if domain.as_bytes().len() > 255 {
|
if domain.as_bytes().len() > 255 {
|
||||||
Err(Error::from(DomainValidationError::DomainTooLong { length: domain.as_bytes().len() }))
|
Err(Error::from(DomainValidationError::DomainTooLong { length: domain.as_bytes().len() }))
|
||||||
} else {
|
} else {
|
||||||
let labels = domain.split('.').collect::<Vec<_>>();
|
let labels = domain.split('.').collect::<Vec<_>>();
|
||||||
|
@ -48,6 +50,7 @@ pub fn normalize_domain(domain_name: &str) -> Result<String, Error> {
|
||||||
|
|
||||||
Ok(domain)
|
Ok(domain)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
pub enum TxtParseError {
|
pub enum TxtParseError {
|
||||||
MissingEscape,
|
MissingEscape,
|
||||||
|
|
Loading…
Reference in a new issue