1use std::time::Duration;
13
14use serde::{Deserialize, Serialize};
15
16use crate::{http::HttpClientBuilder, strip_origin_from_name, DnsRecord, DnsRecordType, IntoFqdn};
17
18pub struct DesecDnsRecordRepresentation {
19 pub record_type: String,
20 pub content: String,
21}
22
23#[derive(Clone)]
24pub struct DesecProvider {
25 client: HttpClientBuilder,
26 endpoint: String,
27}
28
29#[derive(Serialize, Clone, Debug)]
31pub struct DnsRecordParams<'a> {
32 pub subname: &'a str,
33 #[serde(rename = "type")]
34 pub rr_type: &'a str,
35 pub ttl: Option<u32>,
36 pub records: Vec<String>,
37}
38
39#[derive(Deserialize, Debug)]
41pub struct DesecApiResponse {
42 pub created: String,
43 pub domain: String,
44 pub subname: String,
45 pub name: String,
46 pub records: Vec<String>,
47 pub ttl: u32,
48 #[serde(rename = "type")]
49 pub record_type: String,
50 pub touched: String,
51}
52
53#[derive(Deserialize)]
54struct DesecEmptyResponse {}
55
56const DEFAULT_API_ENDPOINT: &str = "https://desec.io/api/v1";
58
59impl DesecProvider {
60 pub(crate) fn new(auth_token: impl AsRef<str>, timeout: Option<Duration>) -> Self {
61 let client = HttpClientBuilder::default()
62 .with_header("Authorization", format!("Token {}", auth_token.as_ref()))
63 .with_timeout(timeout);
64
65 Self {
66 client,
67 endpoint: DEFAULT_API_ENDPOINT.to_string(),
68 }
69 }
70
71 #[cfg(test)]
72 pub(crate) fn with_endpoint(self, endpoint: impl AsRef<str>) -> Self {
73 Self {
74 endpoint: endpoint.as_ref().to_string(),
75 ..self
76 }
77 }
78
79 pub(crate) async fn create(
80 &self,
81 name: impl IntoFqdn<'_>,
82 record: DnsRecord,
83 ttl: u32,
84 origin: impl IntoFqdn<'_>,
85 ) -> crate::Result<()> {
86 let name = name.into_name();
87 let domain = origin.into_name();
88 let subdomain = strip_origin_from_name(&name, &domain);
89
90 let desec_record = DesecDnsRecordRepresentation::from(record);
91 self.client
92 .post(format!(
93 "{endpoint}/domains/{domain}/rrsets/",
94 endpoint = self.endpoint,
95 domain = domain
96 ))
97 .with_body(DnsRecordParams {
98 subname: &subdomain,
99 rr_type: &desec_record.record_type,
100 ttl: Some(ttl),
101 records: vec![desec_record.content],
102 })?
103 .send_with_retry::<DesecApiResponse>(3)
104 .await
105 .map(|_| ())
106 }
107
108 pub(crate) async fn update(
109 &self,
110 name: impl IntoFqdn<'_>,
111 record: DnsRecord,
112 ttl: u32,
113 origin: impl IntoFqdn<'_>,
114 ) -> crate::Result<()> {
115 let name = name.into_name();
116 let domain = origin.into_name();
117 let subdomain = strip_origin_from_name(&name, &domain);
118
119 let desec_record = DesecDnsRecordRepresentation::from(record);
120 self.client
121 .put(format!(
122 "{endpoint}/domains/{domain}/rrsets/{subdomain}/{rr_type}/",
123 endpoint = self.endpoint,
124 domain = &domain,
125 subdomain = &subdomain,
126 rr_type = &desec_record.record_type,
127 ))
128 .with_body(DnsRecordParams {
129 subname: &subdomain,
130 rr_type: desec_record.record_type.as_str(),
131 ttl: Some(ttl),
132 records: vec![desec_record.content],
133 })?
134 .send_with_retry::<DesecApiResponse>(3)
135 .await
136 .map(|_| ())
137 }
138
139 pub(crate) async fn delete(
140 &self,
141 name: impl IntoFqdn<'_>,
142 origin: impl IntoFqdn<'_>,
143 record_type: DnsRecordType,
144 ) -> crate::Result<()> {
145 let name = name.into_name();
146 let domain = origin.into_name();
147 let subdomain = strip_origin_from_name(&name, &domain);
148
149 let rr_type = &record_type.to_string();
150 self.client
151 .delete(format!(
152 "{endpoint}/domains/{domain}/rrsets/{subdomain}/{rtype}/",
153 endpoint = self.endpoint,
154 domain = &domain,
155 subdomain = &subdomain,
156 rtype = &rr_type.to_string(),
157 ))
158 .send_with_retry::<DesecEmptyResponse>(3)
159 .await
160 .map(|_| ())
161 }
162}
163
164impl From<DnsRecord> for DesecDnsRecordRepresentation {
166 fn from(record: DnsRecord) -> Self {
167 match record {
168 DnsRecord::A { content } => DesecDnsRecordRepresentation {
169 record_type: "A".to_string(),
170 content: content.to_string(),
171 },
172 DnsRecord::AAAA { content } => DesecDnsRecordRepresentation {
173 record_type: "AAAA".to_string(),
174 content: content.to_string(),
175 },
176 DnsRecord::CNAME { content } => DesecDnsRecordRepresentation {
177 record_type: "CNAME".to_string(),
178 content,
179 },
180 DnsRecord::NS { content } => DesecDnsRecordRepresentation {
181 record_type: "NS".to_string(),
182 content,
183 },
184 DnsRecord::MX { content, priority } => DesecDnsRecordRepresentation {
185 record_type: "MX".to_string(),
186 content: format!("{priority} {content}"),
187 },
188 DnsRecord::TXT { content } => DesecDnsRecordRepresentation {
189 record_type: "TXT".to_string(),
190 content: format!("\"{content}\""),
191 },
192 DnsRecord::SRV {
193 content,
194 priority,
195 weight,
196 port,
197 } => DesecDnsRecordRepresentation {
198 record_type: "SRV".to_string(),
199 content: format!("{priority} {weight} {port} {content}"),
200 },
201 }
202 }
203}