use std::sync::Arc;
use crate::{
CreateRecord, CreateRecordError, CreateZone, CreateZoneError, DeleteRecord, DeleteRecordError,
DeleteZone, DeleteZoneError, HttpClientConfig, Provider, Record, RecordData,
RetrieveRecordError, RetrieveZoneError, Zone,
};
pub mod api;
const SUPPORTED_RECORD_TYPES: &[&str] = &[
"A", "AAAA", "NS", "MX", "CNAME", "PTR", "TXT", "SRV", "DNAME", "DS", "SSHFP", "TLSA", "SVCB",
"HTTPS", "URI", "CAA", "ANAME", "FWD", "APP",
];
#[derive(Debug)]
pub struct TechnitiumProvider {
api_client: Arc<api::Client>,
}
impl Clone for TechnitiumProvider {
fn clone(&self) -> Self {
TechnitiumProvider {
api_client: Arc::new(self.api_client.as_ref().clone()),
}
}
}
impl TechnitiumProvider {
pub fn new(base_url: &str, token: &str) -> Result<Self, reqwest::Error> {
let api_client = api::Client::new(base_url, token)?;
Ok(Self {
api_client: Arc::new(api_client),
})
}
pub fn with_config(
base_url: &str,
token: &str,
config: HttpClientConfig,
) -> Result<Self, reqwest::Error> {
let api_client = api::Client::with_config(base_url, token, config)?;
Ok(Self {
api_client: Arc::new(api_client),
})
}
pub async fn login(
base_url: &str,
username: &str,
password: &str,
) -> Result<Self, api::ApiError> {
let api_client = api::Client::login(base_url, username, password).await?;
Ok(Self {
api_client: Arc::new(api_client),
})
}
}
impl Provider for TechnitiumProvider {
type Zone = TechnitiumZone;
type CustomRetrieveError = api::ApiError;
async fn get_zone(
&self,
zone_id: &str,
) -> Result<Self::Zone, RetrieveZoneError<Self::CustomRetrieveError>> {
let response = self
.api_client
.get_zone(zone_id)
.await
.map_err(|err| match &err {
api::ApiError::Unauthorized => RetrieveZoneError::Unauthorized,
api::ApiError::NotFound => RetrieveZoneError::NotFound,
_ => RetrieveZoneError::Custom(err),
})?;
Ok(TechnitiumZone {
api_client: self.api_client.clone(),
name: response.name,
zone_type: response.zone_type,
disabled: response.disabled,
})
}
async fn list_zones(
&self,
) -> Result<Vec<Self::Zone>, RetrieveZoneError<Self::CustomRetrieveError>> {
let response = self
.api_client
.list_zones()
.await
.map_err(|err| match &err {
api::ApiError::Unauthorized => RetrieveZoneError::Unauthorized,
api::ApiError::NotFound => RetrieveZoneError::NotFound,
_ => RetrieveZoneError::Custom(err),
})?;
Ok(response
.zones
.into_iter()
.map(|zone| TechnitiumZone {
api_client: self.api_client.clone(),
name: zone.name,
zone_type: zone.zone_type,
disabled: zone.disabled,
})
.collect())
}
}
impl CreateZone for TechnitiumProvider {
type CustomCreateError = api::ApiError;
async fn create_zone(
&self,
domain: &str,
) -> Result<Self::Zone, CreateZoneError<Self::CustomCreateError>> {
let response = self
.api_client
.create_zone(domain)
.await
.map_err(|err| match &err {
api::ApiError::Unauthorized => CreateZoneError::Unauthorized,
api::ApiError::InvalidDomainName => CreateZoneError::InvalidDomainName,
_ => CreateZoneError::Custom(err),
})?;
Ok(TechnitiumZone {
api_client: self.api_client.clone(),
name: response.domain,
zone_type: "Primary".to_string(),
disabled: false,
})
}
}
impl DeleteZone for TechnitiumProvider {
type CustomDeleteError = api::ApiError;
async fn delete_zone(
&self,
zone_id: &str,
) -> Result<(), DeleteZoneError<Self::CustomDeleteError>> {
self.api_client
.delete_zone(zone_id)
.await
.map_err(|err| match &err {
api::ApiError::Unauthorized => DeleteZoneError::Unauthorized,
api::ApiError::NotFound => DeleteZoneError::NotFound,
_ => DeleteZoneError::Custom(err),
})
}
}
#[derive(Debug, Clone)]
pub struct TechnitiumZone {
api_client: Arc<api::Client>,
name: String,
zone_type: String,
disabled: bool,
}
impl TechnitiumZone {
pub fn zone_type(&self) -> &str {
&self.zone_type
}
pub fn is_disabled(&self) -> bool {
self.disabled
}
pub async fn enable(&self) -> Result<(), api::ApiError> {
self.api_client.enable_zone(&self.name).await
}
pub async fn disable(&self) -> Result<(), api::ApiError> {
self.api_client.disable_zone(&self.name).await
}
}
impl Zone for TechnitiumZone {
type CustomRetrieveError = api::ApiError;
fn id(&self) -> &str {
&self.name
}
fn domain(&self) -> &str {
&self.name
}
async fn list_records(
&self,
) -> Result<Vec<Record>, RetrieveRecordError<Self::CustomRetrieveError>> {
let response =
self.api_client
.list_records(&self.name)
.await
.map_err(|err| match &err {
api::ApiError::Unauthorized => RetrieveRecordError::Unauthorized,
api::ApiError::NotFound => RetrieveRecordError::NotFound,
_ => RetrieveRecordError::Custom(err),
})?;
Ok(response.records.into_iter().map(Record::from).collect())
}
async fn get_record(
&self,
record_id: &str,
) -> Result<Record, RetrieveRecordError<Self::CustomRetrieveError>> {
let parts: Vec<&str> = record_id.splitn(3, ':').collect();
if parts.len() < 3 {
return Err(RetrieveRecordError::NotFound);
}
let domain = parts[0];
let record_type = parts[1];
let data_hash = parts[2];
let response = self
.api_client
.get_records(&self.name, domain)
.await
.map_err(|err| match &err {
api::ApiError::Unauthorized => RetrieveRecordError::Unauthorized,
api::ApiError::NotFound => RetrieveRecordError::NotFound,
_ => RetrieveRecordError::Custom(err),
})?;
response
.records
.into_iter()
.map(Record::from)
.find(|r| {
r.data.get_type() == record_type
&& format!("{:x}", calculate_hash(&r.data.get_value())) == data_hash
})
.ok_or(RetrieveRecordError::NotFound)
}
}
impl CreateRecord for TechnitiumZone {
type CustomCreateError = api::ApiError;
async fn create_record(
&self,
host: &str,
data: &RecordData,
ttl: u64,
) -> Result<Record, CreateRecordError<Self::CustomCreateError>> {
let typ = data.get_type();
if !SUPPORTED_RECORD_TYPES.contains(&typ) {
return Err(CreateRecordError::UnsupportedType);
}
let record_params = record_data_to_params(data);
let domain = if host == "@" || host.is_empty() {
self.name.clone()
} else if host.ends_with('.') {
host.trim_end_matches('.').to_string()
} else {
format!("{}.{}", host, self.name)
};
let response = self
.api_client
.add_record(&self.name, &domain, typ, ttl, &record_params)
.await
.map_err(|err| match &err {
api::ApiError::Unauthorized => CreateRecordError::Unauthorized,
api::ApiError::InvalidRecord => CreateRecordError::InvalidRecord,
_ => CreateRecordError::Custom(err),
})?;
Ok(Record::from(response.added_record))
}
}
impl DeleteRecord for TechnitiumZone {
type CustomDeleteError = api::ApiError;
async fn delete_record(
&self,
record_id: &str,
) -> Result<(), DeleteRecordError<Self::CustomDeleteError>> {
let record = self.get_record(record_id).await.map_err(|err| match err {
RetrieveRecordError::Unauthorized => DeleteRecordError::Unauthorized,
RetrieveRecordError::NotFound => DeleteRecordError::NotFound,
RetrieveRecordError::Custom(e) => DeleteRecordError::Custom(e),
})?;
let record_params = record_data_to_params(&record.data);
self.api_client
.delete_record(
&self.name,
&record.host,
record.data.get_type(),
&record_params,
)
.await
.map_err(|err| match &err {
api::ApiError::Unauthorized => DeleteRecordError::Unauthorized,
api::ApiError::NotFound => DeleteRecordError::NotFound,
_ => DeleteRecordError::Custom(err),
})
}
}
impl From<api::Record> for Record {
fn from(record: api::Record) -> Self {
let data = RecordData::from_raw(&record.record_type, &record.rdata.to_value_string());
let data_hash = format!("{:x}", calculate_hash(&data.get_value()));
Record {
id: format!("{}:{}:{}", record.name, record.record_type, data_hash),
host: record.name,
data,
ttl: record.ttl,
}
}
}
fn record_data_to_params(data: &RecordData) -> api::RecordParams {
match data {
RecordData::A(addr) => api::RecordParams::A {
ip_address: addr.to_string(),
},
RecordData::AAAA(addr) => api::RecordParams::AAAA {
ip_address: addr.to_string(),
},
RecordData::CNAME(cname) => api::RecordParams::CNAME {
cname: cname.clone(),
},
RecordData::MX {
priority,
mail_server,
} => api::RecordParams::MX {
preference: *priority,
exchange: mail_server.clone(),
},
RecordData::NS(ns) => api::RecordParams::NS {
name_server: ns.clone(),
},
RecordData::TXT(txt) => api::RecordParams::TXT { text: txt.clone() },
RecordData::SRV {
priority,
weight,
port,
target,
} => api::RecordParams::SRV {
priority: *priority,
weight: *weight,
port: *port,
target: target.clone(),
},
RecordData::Other { value, .. } => api::RecordParams::Other {
value: value.clone(),
},
}
}
fn calculate_hash(s: &str) -> u64 {
use std::hash::{Hash, Hasher};
let mut hasher = std::collections::hash_map::DefaultHasher::new();
s.hash(&mut hasher);
hasher.finish()
}