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