use crate::Error;
use std::str::FromStr;
use crate::error::ResultExt;
use crate::{
HasName, Result,
client::{Client, Url},
};
use serde::{Deserialize, Serialize};
use strum::{Display, EnumString};
const DOMAINS: &str = "domains";
const DNS: &str = "dns";
const ACME_CHALLENGE: &str = "_acme-challenge";
trait UrlDomain {
fn domain_dns(&self, domain_name: &str) -> String;
}
pub trait DnsApi {
fn dns_entry_delete(&mut self, domain_name: &str, entry: DnsEntry) -> Result<()>;
fn dns_entry_delete_all<F>(&mut self, domain_name: &str, f: F) -> Result<()>
where
F: Fn(&DnsEntry) -> bool;
fn dns_entry_list(&mut self, domain_name: &str) -> Result<Vec<DnsEntry>>;
fn dns_entry_insert(&mut self, domain_name: &str, entry: DnsEntry) -> Result<()>;
}
#[derive(Debug, Display, EnumString, PartialEq)]
pub enum RecordType {
A,
AAAA,
ALIAS,
CNAME,
MX,
NS,
PTR,
SOA,
SRV,
TXT,
}
#[derive(Clone, Deserialize, Serialize, Debug, PartialEq)]
pub struct DnsEntry {
pub name: String,
pub expire: u32,
#[serde(rename = "type")]
pub entry_type: String,
pub content: String,
}
impl DnsEntry {
pub fn is_acme_challenge(&self) -> bool {
self.entry_type == *"TXT" && self.name == *ACME_CHALLENGE
}
pub fn new_acme_challenge(expire: u32, content: &str) -> Self {
Self {
name: ACME_CHALLENGE.to_owned(),
expire,
entry_type: RecordType::TXT.to_string(),
content: content.to_owned(),
}
}
}
impl std::fmt::Display for DnsEntry {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{} {} {} {}",
self.name, self.expire, self.entry_type, self.content
)
}
}
impl FromStr for DnsEntry {
type Err = Error;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
let mut splitted = s.split_ascii_whitespace();
let name = splitted
.next()
.ok_or(Error::ParseDnsEntry("name missing"))
.map(String::from)?;
let expire = splitted
.next()
.ok_or(Error::ParseDnsEntry("ttl missing"))
.and_then(|s| s.parse::<u32>().err_into())?;
let entry_type = splitted
.next()
.ok_or(Error::ParseDnsEntry("record type missing"))
.and_then(|s| s.parse::<RecordType>().err_into())
.map(|r| r.to_string())?;
let content = splitted.collect::<Vec<_>>().join(" ");
if content.is_empty() {
return Err(Error::ParseDnsEntry("content missing"));
}
Ok(Self {
name,
expire,
entry_type,
content,
})
}
}
impl HasName for DnsEntry {
fn name(&self) -> &str {
self.name.as_str()
}
}
#[derive(Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct DnsEntryList {
pub dns_entries: Vec<DnsEntry>,
}
#[derive(Deserialize, Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct DnsEntryItem {
pub dns_entry: DnsEntry,
}
impl From<DnsEntry> for DnsEntryItem {
fn from(dns_entry: DnsEntry) -> Self {
Self { dns_entry }
}
}
impl UrlDomain for Url {
fn domain_dns(&self, domain_name: &str) -> String {
format!("{}{}/{}/{}", self.prefix, DOMAINS, domain_name, DNS)
}
}
impl DnsApi for Client {
fn dns_entry_delete(&mut self, domain_name: &str, entry: DnsEntry) -> Result<()> {
self.delete::<DnsEntryItem>(&self.url.domain_dns(domain_name), entry.into())
}
fn dns_entry_delete_all<F>(&mut self, domain_name: &str, f: F) -> Result<()>
where
F: Fn(&DnsEntry) -> bool,
{
for dns_entry in self.dns_entry_list(domain_name)?.into_iter().filter(f) {
self.dns_entry_delete(domain_name, dns_entry)?;
}
Ok(())
}
fn dns_entry_list(&mut self, domain_name: &str) -> Result<Vec<DnsEntry>> {
self.get::<DnsEntryList>(&self.url.domain_dns(domain_name))
.map(|list| list.dns_entries)
}
fn dns_entry_insert(&mut self, domain_name: &str, entry: DnsEntry) -> Result<()> {
self.post::<DnsEntryItem>(&self.url.domain_dns(domain_name), entry.into())
}
}
#[cfg(test)]
mod test {
use super::{DnsApi, DnsEntry, RecordType};
use crate::{Client, HasNames};
#[test]
fn acme_challenge() {
let dns_entry = DnsEntry::new_acme_challenge(60, "Hallo");
assert!(dns_entry.is_acme_challenge());
}
#[test]
fn record_types() {
assert_eq!(RecordType::A.to_string().as_str(), "A");
assert_eq!(RecordType::AAAA.to_string().as_str(), "AAAA");
assert_eq!(RecordType::ALIAS.to_string().as_str(), "ALIAS");
assert_eq!(RecordType::CNAME.to_string().as_str(), "CNAME");
assert_eq!(RecordType::MX.to_string().as_str(), "MX");
assert_eq!(RecordType::NS.to_string().as_str(), "NS");
assert_eq!(RecordType::PTR.to_string().as_str(), "PTR");
assert_eq!(RecordType::SOA.to_string().as_str(), "SOA");
assert_eq!(RecordType::SRV.to_string().as_str(), "SRV");
assert_eq!(RecordType::TXT.to_string().as_str(), "TXT");
}
#[test]
fn record_types_from_str() {
assert_eq!("A".parse::<RecordType>().unwrap(), RecordType::A);
assert_eq!("AAAA".parse::<RecordType>().unwrap(), RecordType::AAAA);
assert_eq!("ALIAS".parse::<RecordType>().unwrap(), RecordType::ALIAS);
assert_eq!("CNAME".parse::<RecordType>().unwrap(), RecordType::CNAME);
assert_eq!("MX".parse::<RecordType>().unwrap(), RecordType::MX);
assert_eq!("NS".parse::<RecordType>().unwrap(), RecordType::NS);
assert_eq!("PTR".parse::<RecordType>().unwrap(), RecordType::PTR);
assert_eq!("SOA".parse::<RecordType>().unwrap(), RecordType::SOA);
assert_eq!("SRV".parse::<RecordType>().unwrap(), RecordType::SRV);
assert_eq!("TXT".parse::<RecordType>().unwrap(), RecordType::TXT);
}
#[test]
fn domain_entry_from_str() {
assert_eq!(
"www 30 A 235.4.3.231".parse::<DnsEntry>().unwrap(),
DnsEntry {
name: "www".to_owned(),
expire: 30,
entry_type: "A".to_owned(),
content: "235.4.3.231".to_owned(),
}
);
assert_eq!(
"_acme-challenge 60 TXT Er is een kindeke"
.parse::<DnsEntry>()
.unwrap(),
DnsEntry {
name: "_acme-challenge".to_owned(),
expire: 60,
entry_type: "TXT".to_owned(),
content: "Er is een kindeke".to_owned(),
}
);
}
#[test]
fn domain_entry_list() {
let entry_list = Client::demo().dns_entry_list("transipdemo.be").unwrap();
let names = entry_list.names();
assert_eq!(
names,
vec![
"*",
"*",
"@",
"@",
"@",
"@",
"transip-A._domainkey",
"transip-B._domainkey",
"transip-C._domainkey",
"_dmarc",
],
);
}
}