use std::{error::Error as StdErr, sync::Arc};
use crate::{
CreateRecord, CreateRecordError, CreateZone, CreateZoneError, DeleteRecord, DeleteRecordError,
DeleteZone, DeleteZoneError, HttpClientConfig, Provider, Record, RecordData,
RetrieveRecordError, RetrieveZoneError, Zone,
};
pub mod api;
pub use api::{ClientConfig, DnspodError, RecordInfoWithTtl, RecordWithTtl};
const SUPPORTED_RECORD_TYPES: &[&str; 9] =
&["A", "AAAA", "CNAME", "MX", "TXT", "NS", "SRV", "URL", "CAA"];
#[derive(Debug)]
pub struct DnspodProvider {
api_client: Arc<api::Client>,
}
impl Clone for DnspodProvider {
fn clone(&self) -> Self {
DnspodProvider {
api_client: Arc::from(self.api_client.as_ref().clone()),
}
}
}
impl DnspodProvider {
pub fn new(login_token: &str, config: &api::ClientConfig) -> Result<Self, Box<dyn StdErr>> {
let api_client = api::Client::new(login_token, config)?;
Ok(Self {
api_client: Arc::new(api_client),
})
}
pub fn with_http_config(
login_token: &str,
client_config: &api::ClientConfig,
http_config: HttpClientConfig,
) -> Result<Self, Box<dyn StdErr>> {
let api_client = api::Client::with_http_config(login_token, client_config, http_config)?;
Ok(Self {
api_client: Arc::new(api_client),
})
}
}
impl Provider for DnspodProvider {
type Zone = DnspodZone;
type CustomRetrieveError = DnspodError;
async fn get_zone(
&self,
zone_id: &str,
) -> Result<Self::Zone, RetrieveZoneError<Self::CustomRetrieveError>> {
let response = if zone_id.contains('.') {
self.api_client.get_domain_by_name(zone_id).await
} else {
self.api_client.get_domain(zone_id).await
}
.map_err(|err| match &err {
DnspodError::Api(status) => {
match status.code.as_str() {
"-1" => RetrieveZoneError::Unauthorized,
"6" | "8" => RetrieveZoneError::NotFound, _ => RetrieveZoneError::Custom(err),
}
}
DnspodError::Request(_) => RetrieveZoneError::Custom(err),
})?;
Ok(DnspodZone {
api_client: self.api_client.clone(),
repr: response
.domain
.expect("domain should be present after success check"),
})
}
async fn list_zones(
&self,
) -> Result<Vec<Self::Zone>, RetrieveZoneError<Self::CustomRetrieveError>> {
let mut zones = Vec::new();
let mut offset: u32 = 0;
const PAGE_SIZE: u32 = 500;
loop {
let result = self
.api_client
.list_domains(Some(offset), Some(PAGE_SIZE))
.await
.map_err(|err| match &err {
DnspodError::Api(status) => match status.code.as_str() {
"-1" => RetrieveZoneError::Unauthorized,
"9" => RetrieveZoneError::NotFound, _ => RetrieveZoneError::Custom(err),
},
DnspodError::Request(_) => RetrieveZoneError::Custom(err),
});
match result {
Ok(response) => {
let domains = response.domains.unwrap_or_default();
let domain_count = domains.len();
zones.extend(domains.into_iter().map(|domain| DnspodZone {
api_client: self.api_client.clone(),
repr: domain,
}));
if domain_count < PAGE_SIZE as usize {
break;
}
offset += PAGE_SIZE;
}
Err(RetrieveZoneError::NotFound) => {
break;
}
Err(err) => return Err(err),
}
}
Ok(zones)
}
}
impl CreateZone for DnspodProvider {
type CustomCreateError = DnspodError;
async fn create_zone(
&self,
domain: &str,
) -> Result<Self::Zone, CreateZoneError<Self::CustomCreateError>> {
let create_response =
self.api_client
.create_domain(domain)
.await
.map_err(|err| match &err {
DnspodError::Api(status) => match status.code.as_str() {
"-1" => CreateZoneError::Unauthorized,
"6" => CreateZoneError::InvalidDomainName, "7" => CreateZoneError::InvalidDomainName, "11" => CreateZoneError::InvalidDomainName, "12" => CreateZoneError::Unauthorized, "41" => CreateZoneError::InvalidDomainName, _ => CreateZoneError::Custom(err),
},
DnspodError::Request(_) => CreateZoneError::Custom(err),
})?;
let domain_response = self
.api_client
.get_domain(&create_response.domain.id)
.await
.map_err(CreateZoneError::Custom)?;
Ok(DnspodZone {
api_client: self.api_client.clone(),
repr: domain_response
.domain
.expect("domain should be present after success check"),
})
}
}
impl DeleteZone for DnspodProvider {
type CustomDeleteError = DnspodError;
async fn delete_zone(
&self,
zone_id: &str,
) -> Result<(), DeleteZoneError<Self::CustomDeleteError>> {
self.api_client
.delete_domain(zone_id)
.await
.map_err(|err| match &err {
DnspodError::Api(status) => match status.code.as_str() {
"-1" => DeleteZoneError::Unauthorized,
"-15" => DeleteZoneError::Unauthorized, "6" => DeleteZoneError::NotFound, "7" => DeleteZoneError::Unauthorized, "8" => DeleteZoneError::Unauthorized, "9" => DeleteZoneError::Unauthorized, _ => DeleteZoneError::Custom(err),
},
DnspodError::Request(_) => DeleteZoneError::Custom(err),
})?;
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct DnspodZone {
api_client: Arc<api::Client>,
repr: api::Domain,
}
impl Zone for DnspodZone {
type CustomRetrieveError = DnspodError;
fn id(&self) -> &str {
&self.repr.id
}
fn domain(&self) -> &str {
&self.repr.name
}
async fn list_records(
&self,
) -> Result<Vec<Record>, RetrieveRecordError<Self::CustomRetrieveError>> {
let mut records = Vec::new();
let mut offset: u32 = 0;
const PAGE_SIZE: u32 = 500;
let default_ttl = self.repr.get_ttl();
loop {
let result = self
.api_client
.list_records(&self.repr.id, Some(offset), Some(PAGE_SIZE))
.await
.map_err(|err| match &err {
DnspodError::Api(status) => match status.code.as_str() {
"-1" => RetrieveRecordError::Unauthorized,
"6" => RetrieveRecordError::NotFound, "9" => RetrieveRecordError::Unauthorized, "10" => RetrieveRecordError::NotFound, _ => RetrieveRecordError::Custom(err),
},
DnspodError::Request(_) => RetrieveRecordError::Custom(err),
});
match result {
Ok(response) => {
let api_records = response.records.unwrap_or_default();
let record_count = api_records.len();
records.extend(
api_records
.iter()
.map(|r| crate::Record::from(api::RecordWithTtl::new(r, default_ttl))),
);
if record_count < PAGE_SIZE as usize {
break;
}
offset += PAGE_SIZE;
}
Err(RetrieveRecordError::NotFound) => {
break;
}
Err(err) => return Err(err),
}
}
Ok(records)
}
async fn get_record(
&self,
record_id: &str,
) -> Result<Record, RetrieveRecordError<Self::CustomRetrieveError>> {
let default_ttl = self.repr.get_ttl();
let response = self
.api_client
.get_record(&self.repr.id, record_id)
.await
.map_err(|err| match &err {
DnspodError::Api(status) => match status.code.as_str() {
"-1" => RetrieveRecordError::Unauthorized,
"6" => RetrieveRecordError::NotFound, "7" => RetrieveRecordError::Unauthorized, "8" => RetrieveRecordError::NotFound, _ => RetrieveRecordError::Custom(err),
},
DnspodError::Request(_) => RetrieveRecordError::Custom(err),
})?;
Ok(crate::Record::from(api::RecordInfoWithTtl::new(
&response.record,
default_ttl,
)))
}
}
impl CreateRecord for DnspodZone {
type CustomCreateError = DnspodError;
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 mx = match data {
RecordData::MX { priority, .. } => Some(*priority),
_ => None,
};
let value = data.get_api_value();
let response = self
.api_client
.create_record(
&self.repr.id,
host,
typ,
"default", &value,
mx,
Some(ttl),
)
.await
.map_err(|err| match &err {
DnspodError::Api(status) => match status.code.as_str() {
"-1" => CreateRecordError::Unauthorized,
"-15" => CreateRecordError::Unauthorized, "6" => CreateRecordError::InvalidRecord, "7" => CreateRecordError::Unauthorized, "21" => CreateRecordError::Unauthorized, "22" | "23" | "24" | "25" => CreateRecordError::InvalidRecord, "26" => CreateRecordError::InvalidRecord, "27" => CreateRecordError::UnsupportedType, "30" => CreateRecordError::InvalidRecord, "31" | "32" | "33" => CreateRecordError::InvalidRecord, "34" => CreateRecordError::InvalidRecord, _ => CreateRecordError::Custom(err),
},
DnspodError::Request(_) => CreateRecordError::Custom(err),
})?;
let record_data = response.record.ok_or_else(|| {
CreateRecordError::Custom(DnspodError::Api(api::Status {
code: "0".to_string(),
message: "API returned success but no record data".to_string(),
created_at: None,
}))
})?;
Ok(Record {
id: record_data.id,
host: host.to_string(),
data: data.clone(),
ttl,
})
}
}
impl DeleteRecord for DnspodZone {
type CustomDeleteError = DnspodError;
async fn delete_record(
&self,
record_id: &str,
) -> Result<(), DeleteRecordError<Self::CustomDeleteError>> {
self.api_client
.delete_record(&self.repr.id, record_id)
.await
.map_err(|err| match &err {
DnspodError::Api(status) => match status.code.as_str() {
"-1" => DeleteRecordError::Unauthorized,
"-15" => DeleteRecordError::Unauthorized, "6" => DeleteRecordError::NotFound, "7" => DeleteRecordError::Unauthorized, "8" => DeleteRecordError::NotFound, "21" => DeleteRecordError::Unauthorized, _ => DeleteRecordError::Custom(err),
},
DnspodError::Request(_) => DeleteRecordError::Custom(err),
})?;
Ok(())
}
}