#![deny(rustdoc::broken_intra_doc_links)]
#![forbid(unsafe_code)]
use std::{
fmt::Debug,
future::Future,
net::{Ipv4Addr, Ipv6Addr},
str::FromStr,
};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use thiserror::Error;
#[cfg(feature = "hetzner")]
pub mod hetzner;
pub trait Provider {
type Zone: Zone;
type CustomRetrieveError: Debug;
fn list_zones(
&self,
) -> impl Future<Output = Result<Vec<Self::Zone>, RetrieveZoneError<Self::CustomRetrieveError>>>;
fn get_zone(
&self,
zone_id: &str,
) -> impl Future<Output = Result<Self::Zone, RetrieveZoneError<Self::CustomRetrieveError>>>;
}
#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, Error)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum RetrieveZoneError<T> {
#[error("the DNS provider is unauthorized")]
Unauthorized,
#[error("the requested zone was not found")]
NotFound,
#[error(transparent)]
Custom(#[from] T),
}
pub trait CreateZone: Provider {
type CustomCreateError: Debug;
fn create_zone(
&self,
domain: &str,
) -> impl Future<Output = Result<Self::Zone, CreateZoneError<Self::CustomCreateError>>>;
}
#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, Error)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum CreateZoneError<T> {
#[error("the DNS provider is unauthorized")]
Unauthorized,
#[error("the given domain name is invalid")]
InvalidDomainName,
#[error(transparent)]
Custom(#[from] T),
}
pub trait DeleteZone: Provider {
type CustomDeleteError: Debug;
fn delete_zone(
&self,
zone_id: &str,
) -> impl Future<Output = Result<(), DeleteZoneError<Self::CustomDeleteError>>>;
}
#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, Error)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum DeleteZoneError<T> {
#[error("the DNS provider is unauthorized")]
Unauthorized,
#[error("the requested zone was not found")]
NotFound,
#[error(transparent)]
Custom(#[from] T),
}
#[derive(Debug, PartialEq, Eq, Clone, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum RecordData {
A(Ipv4Addr),
AAAA(Ipv6Addr),
CNAME(String),
MX {
priority: u16,
mail_server: String,
},
NS(String),
SRV {
priority: u16,
weight: u16,
port: u16,
target: String,
},
TXT(String),
Other {
typ: String,
value: String,
},
}
impl RecordData {
pub fn from_raw(typ: &str, value: &str) -> RecordData {
let data = match typ {
"A" => Ipv4Addr::from_str(value).ok().map(RecordData::A),
"AAAA" => Ipv6Addr::from_str(value).ok().map(RecordData::AAAA),
"CNAME" => Some(RecordData::CNAME(value.to_owned())),
"MX" => {
let mut iter = value.split_whitespace();
let opt_priority = iter.next().and_then(|raw| raw.parse::<u16>().ok());
let opt_server = iter.next();
match (opt_priority, opt_server) {
(Some(priority), Some(server)) => Some(RecordData::MX {
priority,
mail_server: server.to_owned(),
}),
_ => None,
}
}
"NS" => Some(RecordData::NS(value.to_owned())),
"SRV" => {
let mut iter = value.split_whitespace();
let opt_priority = iter.next().and_then(|raw| raw.parse::<u16>().ok());
let opt_weight = iter.next().and_then(|raw| raw.parse::<u16>().ok());
let opt_port = iter.next().and_then(|raw| raw.parse::<u16>().ok());
let opt_target = iter.next();
match (opt_priority, opt_weight, opt_port, opt_target) {
(Some(priority), Some(weight), Some(port), Some(target)) => {
Some(RecordData::SRV {
priority,
weight,
port,
target: target.to_owned(),
})
}
_ => None,
}
}
"TXT" => Some(RecordData::TXT(value.to_owned())),
_ => None,
};
data.unwrap_or(RecordData::Other {
typ: typ.to_owned(),
value: value.to_owned(),
})
}
pub fn get_type(&self) -> &str {
match self {
RecordData::A(_) => "A",
RecordData::AAAA(_) => "A",
RecordData::CNAME(_) => "CNAME",
RecordData::MX { .. } => "MX",
RecordData::NS(_) => "NS",
RecordData::SRV { .. } => "SRV",
RecordData::TXT(_) => "TXT",
RecordData::Other { typ, .. } => typ.as_str(),
}
}
pub fn get_value(&self) -> String {
match self {
RecordData::A(addr) => addr.to_string(),
RecordData::AAAA(addr) => addr.to_string(),
RecordData::CNAME(alias) => alias.clone(),
RecordData::MX {
priority,
mail_server,
} => format!("{} {}", priority, mail_server),
RecordData::NS(ns) => ns.clone(),
RecordData::SRV {
priority,
weight,
port,
target,
} => format!("{} {} {} {}", priority, weight, port, target),
RecordData::TXT(val) => val.clone(),
RecordData::Other { value, .. } => value.clone(),
}
}
}
#[derive(Debug, PartialEq, Eq, Clone, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Record {
pub id: String,
pub host: String,
pub data: RecordData,
pub ttl: u64,
}
pub trait Zone {
type CustomRetrieveError: Debug;
fn id(&self) -> &str;
fn domain(&self) -> &str;
fn list_records(
&self,
) -> impl Future<Output = Result<Vec<Record>, RetrieveRecordError<Self::CustomRetrieveError>>>;
fn get_record(
&self,
record_id: &str,
) -> impl Future<Output = Result<Record, RetrieveRecordError<Self::CustomRetrieveError>>>;
}
#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, Error)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum RetrieveRecordError<T> {
#[error("the DNS provider is unauthorized")]
Unauthorized,
#[error("the requested record was not found")]
NotFound,
#[error(transparent)]
Custom(#[from] T),
}
pub trait CreateRecord: Zone {
type CustomCreateError: Debug;
fn create_record(
&self,
host: &str,
data: &RecordData,
ttl: u64,
) -> impl Future<Output = Result<Record, CreateRecordError<Self::CustomCreateError>>>;
}
#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, Error)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum CreateRecordError<T> {
#[error("the DNS provider is unauthorized")]
Unauthorized,
#[error("the DNS provider does not support the specified record type")]
UnsupportedType,
#[error("the given record value is invalid")]
InvalidRecord,
#[error(transparent)]
Custom(#[from] T),
}
pub trait DeleteRecord: Zone {
type CustomDeleteError: Debug;
fn delete_record(
&self,
record_id: &str,
) -> impl Future<Output = Result<(), DeleteRecordError<Self::CustomDeleteError>>>;
}
#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, Error)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum DeleteRecordError<T> {
#[error("the DNS provider is unauthorized")]
Unauthorized,
#[error("the requested record was not found")]
NotFound,
#[error(transparent)]
Custom(#[from] T),
}