dns_update/providers/
desec.rs

1/*
2 * Copyright Stalwart Labs LLC See the COPYING
3 * file at the top-level directory of this distribution.
4 *
5 * Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
6 * https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
7 * <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
8 * option. This file may not be copied, modified, or distributed
9 * except according to those terms.
10 */
11
12use 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/// The parameters for creation and modification requests of the desec API.
30#[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/// The response for creation and modification requests of the desec API.
40#[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
56/// The default endpoint for the desec API.
57const 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
164/// Converts a DNS record into a representation that can be sent to the desec API.
165impl 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}