dns_update/providers/
digitalocean.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::{
13    net::{Ipv4Addr, Ipv6Addr},
14    time::Duration,
15};
16
17use crate::{http::HttpClientBuilder, strip_origin_from_name, DnsRecord, Error, IntoFqdn};
18use serde::{Deserialize, Serialize};
19
20#[derive(Clone)]
21pub struct DigitalOceanProvider {
22    client: HttpClientBuilder,
23}
24
25#[derive(Deserialize, Serialize, Clone, Debug)]
26pub struct ListDomainRecord {
27    domain_records: Vec<DomainRecord>,
28}
29
30#[derive(Deserialize, Serialize, Clone, Debug)]
31pub struct UpdateDomainRecord<'a> {
32    ttl: u32,
33    name: &'a str,
34    #[serde(flatten)]
35    data: RecordData,
36}
37
38#[derive(Deserialize, Serialize, Clone, Debug)]
39pub struct DomainRecord {
40    id: i64,
41    ttl: u32,
42    name: String,
43    #[serde(flatten)]
44    data: RecordData,
45}
46
47#[derive(Deserialize, Serialize, Clone, Debug)]
48#[serde(tag = "type")]
49#[allow(clippy::upper_case_acronyms)]
50pub enum RecordData {
51    A {
52        data: Ipv4Addr,
53    },
54    AAAA {
55        data: Ipv6Addr,
56    },
57    CNAME {
58        data: String,
59    },
60    NS {
61        data: String,
62    },
63    MX {
64        data: String,
65        priority: u16,
66    },
67    TXT {
68        data: String,
69    },
70    SRV {
71        data: String,
72        priority: u16,
73        port: u16,
74        weight: u16,
75    },
76}
77
78#[derive(Serialize, Debug)]
79pub struct Query<'a> {
80    name: &'a str,
81}
82
83impl DigitalOceanProvider {
84    pub(crate) fn new(auth_token: impl AsRef<str>, timeout: Option<Duration>) -> Self {
85        let client = HttpClientBuilder::default()
86            .with_header("Authorization", format!("Bearer {}", auth_token.as_ref()))
87            .with_timeout(timeout);
88        Self { client }
89    }
90
91    pub(crate) async fn create(
92        &self,
93        name: impl IntoFqdn<'_>,
94        record: DnsRecord,
95        ttl: u32,
96        origin: impl IntoFqdn<'_>,
97    ) -> crate::Result<()> {
98        let name = name.into_name();
99        let domain = origin.into_name();
100        let subdomain = strip_origin_from_name(&name, &domain);
101
102        self.client
103            .post(format!(
104                "https://api.digitalocean.com/v2/domains/{domain}/records",
105            ))
106            .with_body(UpdateDomainRecord {
107                ttl,
108                name: &subdomain,
109                data: record.into(),
110            })?
111            .send_raw()
112            .await
113            .map(|_| ())
114    }
115
116    pub(crate) async fn update(
117        &self,
118        name: impl IntoFqdn<'_>,
119        record: DnsRecord,
120        ttl: u32,
121        origin: impl IntoFqdn<'_>,
122    ) -> crate::Result<()> {
123        let name = name.into_name();
124        let domain = origin.into_name();
125        let subdomain = strip_origin_from_name(&name, &domain);
126        let record_id = self.obtain_record_id(&name, &domain).await?;
127
128        self.client
129            .put(format!(
130                "https://api.digitalocean.com/v2/domains/{domain}/records/{record_id}",
131            ))
132            .with_body(UpdateDomainRecord {
133                ttl,
134                name: &subdomain,
135                data: record.into(),
136            })?
137            .send_raw()
138            .await
139            .map(|_| ())
140    }
141
142    pub(crate) async fn delete(
143        &self,
144        name: impl IntoFqdn<'_>,
145        origin: impl IntoFqdn<'_>,
146    ) -> crate::Result<()> {
147        let name = name.into_name();
148        let domain = origin.into_name();
149        let record_id = self.obtain_record_id(&name, &domain).await?;
150
151        self.client
152            .delete(format!(
153                "https://api.digitalocean.com/v2/domains/{domain}/records/{record_id}",
154            ))
155            .send_raw()
156            .await
157            .map(|_| ())
158    }
159
160    async fn obtain_record_id(&self, name: &str, domain: &str) -> crate::Result<i64> {
161        let subdomain = strip_origin_from_name(name, domain);
162        self.client
163            .get(format!(
164                "https://api.digitalocean.com/v2/domains/{domain}/records?{}",
165                Query::name(name).serialize()
166            ))
167            .send_with_retry::<ListDomainRecord>(3)
168            .await
169            .and_then(|result| {
170                result
171                    .domain_records
172                    .into_iter()
173                    .find(|record| record.name == subdomain)
174                    .map(|record| record.id)
175                    .ok_or_else(|| Error::Api(format!("DNS Record {} not found", subdomain)))
176            })
177    }
178}
179
180impl<'a> Query<'a> {
181    pub fn name(name: impl Into<&'a str>) -> Self {
182        Self { name: name.into() }
183    }
184
185    pub fn serialize(&self) -> String {
186        serde_urlencoded::to_string(self).unwrap()
187    }
188}
189
190impl From<DnsRecord> for RecordData {
191    fn from(record: DnsRecord) -> Self {
192        match record {
193            DnsRecord::A { content } => RecordData::A { data: content },
194            DnsRecord::AAAA { content } => RecordData::AAAA { data: content },
195            DnsRecord::CNAME { content } => RecordData::CNAME { data: content },
196            DnsRecord::NS { content } => RecordData::NS { data: content },
197            DnsRecord::MX { content, priority } => RecordData::MX {
198                data: content,
199                priority,
200            },
201            DnsRecord::TXT { content } => RecordData::TXT { data: content },
202            DnsRecord::SRV {
203                content,
204                priority,
205                weight,
206                port,
207            } => RecordData::SRV {
208                data: content,
209                priority,
210                weight,
211                port,
212            },
213        }
214    }
215}