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]]
|
||||
name = "axum"
|
||||
version = "0.8.0-rc.1"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47b212eb691030da38f1eff381777e431eb6f0760a0d02ffcb1702f1da9894e2"
|
||||
checksum = "6d6fd624c75e18b3b4c6b9caf42b1afe24437daaee904069137d8bab077be8b8"
|
||||
dependencies = [
|
||||
"axum-core",
|
||||
"bytes",
|
||||
"form_urlencoded",
|
||||
"futures-util",
|
||||
"http",
|
||||
"http-body",
|
||||
|
@ -100,9 +101,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "axum-core"
|
||||
version = "0.5.0-rc.1"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "30256e79153b08607dcf9e0a72bd31564bc9228b9f145d1e1a29e3d01ad1fd16"
|
||||
checksum = "df1362f362fd16024ae199c1970ce98f9661bf5ef94b9808fee734bc3698b733"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-util",
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
name = "nomilo"
|
||||
version = "0.2.0-dev"
|
||||
authors = ["DNS Witch Collective <dns-witch@dns-witch.eu.org>"]
|
||||
edition = "2021"
|
||||
edition = "2024"
|
||||
license = "AGPL-3.0-or-later"
|
||||
readme = "README.md"
|
||||
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"
|
||||
tera = { version = "1", default-features = false }
|
||||
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" }
|
||||
rusqlite = { version = "0.32"}
|
||||
async-trait = { version = "0.1" }
|
||||
|
|
|
@ -23,6 +23,14 @@ main {
|
|||
margin: auto;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-weight: 200;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
article.domain {
|
||||
margin-bottom: 2em;
|
||||
}
|
||||
|
@ -38,7 +46,7 @@ article.domain header h3.folder-tab {
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding: 0 1em;
|
||||
padding: 0 1rem;
|
||||
border-top-left-radius: .3rem;
|
||||
background-color: #f2e0fd;
|
||||
margin: 0;
|
||||
|
@ -52,26 +60,23 @@ article.domain header h3.folder-tab ~ .sep {
|
|||
background-color: #f2e0fd;
|
||||
height: 100%;
|
||||
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 {
|
||||
background: #f2e0fd;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
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 {
|
||||
|
@ -80,18 +85,8 @@ article.domain .records .rrset .rtype {
|
|||
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 {
|
||||
padding: 0;
|
||||
padding: 1rem 0 1rem 2rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: .5rem;
|
||||
|
@ -105,32 +100,16 @@ article.domain .records .rrset li {
|
|||
gap: .5rem;
|
||||
}
|
||||
|
||||
article.domain .records .rrset li::before {
|
||||
content: '';
|
||||
height: 1em;
|
||||
width: 1rem;
|
||||
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 {
|
||||
display: flex;
|
||||
gap: .2rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
article.domain .records .rrset .rdata-main {
|
||||
display: flex;
|
||||
gap: .3rem;
|
||||
margin-right: .1rem;
|
||||
}
|
||||
|
||||
article.domain .records .rrset .rdata-main .pill {
|
||||
|
@ -138,7 +117,6 @@ article.domain .records .rrset .rdata-main .pill {
|
|||
}
|
||||
|
||||
article.domain .records .rrset .rdata-complementary {
|
||||
margin-top: .2em;
|
||||
font-size: .9em;
|
||||
gap: .2rem;
|
||||
display: flex;
|
||||
|
|
|
@ -6,6 +6,12 @@ example.com. IN SOA ns.example.com. admin.example.com. (
|
|||
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.
|
||||
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::post(routes::api::zones::create_zone_records))
|
||||
/* ----- 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"))
|
||||
.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 use rdata::*;
|
||||
|
|
|
@ -1,98 +1,309 @@
|
|||
use std::fmt;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde::{Deserialize, Serialize, Serializer, ser::SerializeStruct};
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub enum FriendlyRData {
|
||||
Address(Address),
|
||||
Service(Service),
|
||||
MailServer(MailServer), // Include SPF, etc ?!
|
||||
NameServer(NameServer),
|
||||
TextData(TextData),
|
||||
Alias(Alias),
|
||||
use crate::resources::dns::internal;
|
||||
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, PartialEq)]
|
||||
#[serde(rename_all="lowercase")]
|
||||
pub enum FriendlyRType {
|
||||
Address,
|
||||
Service,
|
||||
Alias,
|
||||
MailServer,
|
||||
NameServer,
|
||||
Service,
|
||||
Spf,
|
||||
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,
|
||||
friendly_type: FriendlyRType,
|
||||
rdata: FriendlyRData,
|
||||
ttl: u32,
|
||||
general_records: Vec<FriendlyRecordSet<FriendlyRData>>,
|
||||
mail: Option<MailConfiguration>,
|
||||
web: Option<WebConfiguration>,
|
||||
}
|
||||
|
||||
pub struct RecordSet {
|
||||
friendly_type: FriendlyRType,
|
||||
records: Vec<FriendlyRecord>
|
||||
impl FriendlyRecordSetGroup {
|
||||
pub fn new(owner: String) -> Self {
|
||||
FriendlyRecordSetGroup {
|
||||
owner,
|
||||
general_records: Vec::new(),
|
||||
mail: None,
|
||||
web: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RecordSetGroup {
|
||||
owner: String,
|
||||
rrsets: Vec<RecordSet>
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct FriendlyRecords {
|
||||
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)]
|
||||
pub struct Address {
|
||||
pub address: String
|
||||
}
|
||||
|
||||
|
||||
impl HasRType for Address {
|
||||
fn rtype(&self) -> FriendlyRType {
|
||||
FriendlyRType::Address
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct Service {
|
||||
pub service_type: String,
|
||||
pub port: u16,
|
||||
pub weight: u16,
|
||||
pub priority: u16,
|
||||
pub service_type: ServiceType,
|
||||
pub service_name: Option<String>,
|
||||
pub service_protocol: Option<String>,
|
||||
pub port: i64,
|
||||
pub weight: i64,
|
||||
pub priority: i64,
|
||||
pub server: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum ServiceType {
|
||||
ServiceProtocol { service_name: String, protocol: String },
|
||||
Service { service_name: String },
|
||||
None,
|
||||
Other,
|
||||
}
|
||||
|
||||
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)]
|
||||
pub struct MailServer {
|
||||
pub preference: u16,
|
||||
pub preference: i64,
|
||||
pub mail_exchanger: String,
|
||||
}
|
||||
|
||||
impl HasRType for MailServer {
|
||||
fn rtype(&self) -> FriendlyRType {
|
||||
FriendlyRType::MailServer
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct NameServer {
|
||||
pub target: String,
|
||||
|
@ -105,5 +316,18 @@ pub struct TextData {
|
|||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct Alias {
|
||||
pub from: 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 crate::resources::dns::friendly;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum RData {
|
||||
A(A),
|
||||
|
@ -29,6 +31,18 @@ impl RData {
|
|||
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)]
|
||||
|
@ -36,27 +50,70 @@ pub struct A {
|
|||
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)]
|
||||
pub struct Aaaa {
|
||||
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)]
|
||||
pub struct Cname {
|
||||
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)]
|
||||
pub struct Mx {
|
||||
pub preference: u16,
|
||||
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)]
|
||||
pub struct Ns {
|
||||
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)]
|
||||
pub struct Ptr {
|
||||
pub target: Name,
|
||||
|
@ -81,6 +138,32 @@ pub struct Srv {
|
|||
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)]
|
||||
pub struct Txt {
|
||||
pub text: Vec<u8>,
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
pub mod external;
|
||||
pub mod internal;
|
||||
//pub mod friendly;
|
||||
pub mod friendly;
|
||||
|
|
|
@ -41,9 +41,8 @@ impl Zone {
|
|||
db.create_zone(create_zone).await
|
||||
}
|
||||
|
||||
pub async fn get_records(zone_name: &str, db: BoxedDb, record_driver: BoxedRecordDriver) ->Result<RecordList, Error> {
|
||||
let zone = db.get_zone_by_name(zone_name).await?;
|
||||
let mut records = record_driver.get_records(&zone.name).await?;
|
||||
pub async fn get_records(&self, record_driver: BoxedRecordDriver) ->Result<RecordList, Error> {
|
||||
let mut records = record_driver.get_records(&self.name).await?;
|
||||
|
||||
records.sort();
|
||||
|
||||
|
|
|
@ -21,7 +21,8 @@ pub async fn get_zone_records(
|
|||
State(app): State<AppState>,
|
||||
) -> 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
|
||||
.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 crate::validation;
|
||||
use crate::AppState;
|
||||
use crate::errors::Error;
|
||||
use crate::resources::zone::Zone;
|
||||
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>,
|
||||
State(app): State<AppState>,
|
||||
request: Request,
|
||||
) -> Result<Template<'static, Value>, Error> {
|
||||
let records = Zone::get_records(&zone_name, app.db, app.records).await?;
|
||||
|
||||
//records.0.sort_by_key(|record| (&record.name, record.rdata));
|
||||
let zone = app.db.get_zone_by_name(&zone_name).await?;
|
||||
let records = zone.get_records(app.records).await?;
|
||||
let records = friendly::FriendlyRecords::from(records.clone());
|
||||
|
||||
Ok(Template::new(
|
||||
"pages/records.html",
|
||||
app.template_engine,
|
||||
json!({
|
||||
"current_zone": zone_name,
|
||||
"records": external::RecordList::from(records),
|
||||
"current_zone": zone.name,
|
||||
"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
|
||||
/// TODO: No support of dots in labels, how to handle RNAME in SOA?
|
||||
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() {
|
||||
Err(Error::from(DomainValidationError::EmptyDomain))
|
||||
} else if domain.as_bytes().len() > 255 {
|
||||
let domain = domain_name.strip_suffix('.').unwrap_or(domain_name).to_lowercase();
|
||||
|
||||
if domain.as_bytes().len() > 255 {
|
||||
Err(Error::from(DomainValidationError::DomainTooLong { length: domain.as_bytes().len() }))
|
||||
} else {
|
||||
let labels = domain.split('.').collect::<Vec<_>>();
|
||||
|
@ -48,6 +50,7 @@ pub fn normalize_domain(domain_name: &str) -> Result<String, Error> {
|
|||
|
||||
Ok(domain)
|
||||
}
|
||||
|
||||
}
|
||||
pub enum TxtParseError {
|
||||
MissingEscape,
|
||||
|
|
Loading…
Reference in a new issue