use addr::parse_domain_name;
use reqwest::{StatusCode};
use serde::{Deserialize, Serialize};
use crate::Client;
use crate::Error;
use crate::error::PowerDNSResponseError;
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[serde_with::skip_serializing_none]
pub struct Zone {
pub id: Option<String>,
pub name: Option<String>,
#[serde(rename = "type")]
pub type_field: Option<String>,
pub url: Option<String>,
pub kind: Option<ZoneKind>,
pub rrsets: Option<Vec<RRSet>>,
pub serial: Option<u32>,
pub notified_serial: Option<u32>,
pub edited_serial: Option<u32>,
pub masters: Option<Vec<String>>,
pub dnssec: Option<bool>,
pub nsec3param: Option<String>,
pub nsec3narrow: Option<bool>,
pub presigned: Option<bool>,
pub soa_edit: Option<String>,
pub soa_edit_api: Option<String>,
pub api_rectify: Option<bool>,
pub zone: Option<String>,
pub account: Option<String>,
pub nameservers: Option<Vec<String>>,
pub master_tsig_key_ids: Option<Vec<String>>,
pub slave_tsig_key_ids: Option<Vec<String>>,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
pub enum ZoneKind {
Native,
Master,
Slave,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
pub struct PatchZone {
pub rrsets: Vec<RRSet>
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[serde_with::skip_serializing_none]
pub struct RRSet {
pub name: String,
#[serde(rename = "type")]
pub type_field: String,
pub ttl: u32,
pub changetype: Option<String>,
pub records: Vec<Record>,
pub comments: Option<Vec<Comment>>,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[serde_with::skip_serializing_none]
pub struct Record {
pub content: String,
pub disabled: Option<bool>,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[serde_with::skip_serializing_none]
pub struct Comment {
pub content: String,
pub account: String,
pub modified_at: u32,
}
pub struct ZoneClient<'a> {
api_client: &'a Client,
}
impl<'a> ZoneClient<'a> {
pub fn new(api_client: &'a Client) -> Self {
ZoneClient { api_client }
}
pub async fn list(&self) -> Result<Vec<Zone>, Error> {
let resp = self
.api_client
.http_client
.get(format!(
"{}/api/v1/servers/{}/zones",
self.api_client.base_url, self.api_client.server_name
))
.send()
.await
.unwrap();
if resp.status().is_success() {
Ok(resp.json::<Vec<Zone>>().await?)
} else {
Err(resp.json::<PowerDNSResponseError>().await?)?
}
}
pub async fn get(&self, zone_id: &str) -> Result<Zone, Error> {
let zone_id = canonicalize_domain(zone_id).unwrap();
let resp = self
.api_client
.http_client
.get(format!(
"{}/api/v1/servers/{}/zones/{zone_id}",
self.api_client.base_url, self.api_client.server_name
))
.send()
.await
.unwrap();
if resp.status().is_success() {
Ok(resp.json::<Zone>().await?)
} else {
Err(resp.json::<PowerDNSResponseError>().await?)?
}
}
pub async fn delete(&self, zone_id: &str) -> Result<(), Error> {
let zone_id = canonicalize_domain(zone_id).unwrap();
let resp = self
.api_client
.http_client
.delete(format!(
"{}/api/v1/servers/{}/zones/{zone_id}",
self.api_client.base_url, self.api_client.server_name
))
.send()
.await
.unwrap();
if resp.status().is_success() {
Ok(())
} else {
Err(resp.json::<PowerDNSResponseError>().await?)?
}
}
pub async fn patch(&self, zone_id: &str, zone: PatchZone) -> Result<(), Error> {
let response = self
.api_client
.http_client
.patch(
format!("{}/api/v1/servers/{}/zones/{zone_id}",
self.api_client.base_url,
self.api_client.server_name,
))
.json(&zone)
.send()
.await?;
match response.status() {
StatusCode::NO_CONTENT => Ok(()),
StatusCode::BAD_REQUEST | StatusCode::NOT_FOUND |
StatusCode::UNPROCESSABLE_ENTITY | StatusCode::INTERNAL_SERVER_ERROR => {
Err(Error::PowerDNS(response.json().await?))
},
status => Err(Error::UnexpectedStatusCode(status)),
}
}
}
fn canonicalize_domain(domain: &str) -> Result<String, ()> {
let parsed = match parse_domain_name(domain) {
Ok(p) => p,
Err(_) => return Err(()),
};
let mut root = parsed.as_str().to_string();
if !parsed.has_known_suffix() {
return Err(());
}
if !root.ends_with('.') {
root += ".";
}
Ok(root)
}
#[cfg(test)]
mod tests {
use crate::zones::canonicalize_domain;
#[test]
fn already_canonical() {
let root = canonicalize_domain("powerdns.com.").unwrap();
assert_eq!(root, "powerdns.com.")
}
#[test]
fn not_yet_canonical() {
let root = canonicalize_domain("powerdns.com").unwrap();
assert_eq!(root, "powerdns.com.")
}
#[test]
fn not_top_level() {
let root = canonicalize_domain("doc.powerdns.com").unwrap();
assert_eq!(root, "doc.powerdns.com.")
}
}