1#![allow(unused)]
3
4mod types;
5
6use std::{net::Ipv4Addr, sync::Arc};
7use async_lock::Mutex;
8use cfg_if::cfg_if;
9use hyper::Uri;
10use serde::de::DeserializeOwned;
11use tracing::{error, info, warn};
12
13
14use crate::{dnsimple::types::{Accounts, CreateRecord, GetRecord, Records, UpdateRecord}, errors::{Error, Result}, http, Config, DnsProvider, RecordType};
15
16
17const API_BASE: &str = "https:://api.dnsimple.com/v2";
18
19pub struct Auth {
20 key: String,
21}
22
23impl Auth {
24 fn get_header(&self) -> String {
25 format!("Bearer {}", self.key)
26 }
27}
28
29struct DnSimple {
30 config: Config,
31 endpoint: &'static str,
32 auth: Auth,
33 acc_id: Mutex<Option<u32>>,
34}
35
36impl DnSimple {
37 pub fn new(config: Config, auth: Auth, acc: Option<u32>) -> Self {
38 Self::new_with_endpoint(config, auth, acc, API_BASE)
39 }
40
41 fn new_with_endpoint(config: Config, auth: Auth, acc: Option<u32>, endpoint: &'static str) -> Self {
42 let acc_id = Mutex::new(acc);
43 DnSimple {
44 config,
45 endpoint,
46 auth,
47 acc_id,
48 }
49 }
50
51 async fn get_upstream_id(&self) -> Result<u32> {
52 info!("Fetching account ID from upstream");
53 let endpoint = format!("{}/accounts", self.endpoint);
54 let uri = endpoint.parse()
55 .map_err(|e| Error::UrlError(format!("Error: {endpoint} -> {e}")))?;
56
57 let accounts_p = http::get::<Accounts>(uri, Some(self.auth.get_header())).await?;
58
59 match accounts_p {
60 Some(accounts) if accounts.accounts.len() == 1 => {
61 Ok(accounts.accounts[0].id)
62 }
63 Some(accounts) if accounts.accounts.len() > 1 => {
64 Err(Error::ApiError("More than one account returned; you must specify the account ID to use".to_string()))
65 }
66 _ => {
68 Err(Error::ApiError("No accounts returned from upstream".to_string()))
69 }
70 }
71 }
72
73 async fn get_id(&self) -> Result<u32> {
74 let mut id_p = self.acc_id.lock().await;
78
79 if let Some(id) = *id_p {
80 return Ok(id);
81 }
82
83 let id = self.get_upstream_id().await?;
84 *id_p = Some(id);
85
86 Ok(id)
87 }
88
89 async fn get_record(&self, host: &str, rtype: RecordType) -> Result<Option<GetRecord>>
90 {
91 let acc_id = self.get_id().await?;
92
93 let url = format!("{}/{acc_id}/zones/{}/records?name={host}&type={rtype}", self.endpoint, self.config.domain)
94 .parse()
95 .map_err(|e| Error::UrlError(format!("Error: {e}")))?;
96
97 let auth = self.auth.get_header();
98 let recs: Records = match http::get(url, Some(auth)).await? {
99 Some(rec) => rec,
100 None => return Ok(None)
101 };
102
103 let nr = recs.records.len();
106 if nr > 1 {
107 error!("Returned number of IPs is {}, should be 1", nr);
108 return Err(Error::UnexpectedRecord(format!("Returned number of IPs is {nr}, should be 1")));
109 } else if nr == 0 {
110 warn!("No IP returned for {host}, continuing");
111 return Ok(None);
112 }
113
114 Ok(Some(recs.records[0].clone()))
115 }
116}
117
118
119impl DnsProvider for DnSimple {
120
121 async fn get_v4_record(&self, host: &str) -> Result<Option<Ipv4Addr> > {
122 let rec: GetRecord = match self.get_record(host, RecordType::A).await? {
123 Some(recs) => recs,
124 None => return Ok(None)
125 };
126
127
128 Ok(Some(rec.content))
129 }
130
131 async fn create_v4_record(&self, host: &str, ip: &Ipv4Addr) -> Result<()> {
132 let acc_id = self.get_id().await?;
133
134 let url = format!("{}/{acc_id}/zones/{}/records", self.endpoint, self.config.domain)
135 .parse()
136 .map_err(|e| Error::UrlError(format!("Error: {e}")))?;
137 let auth = self.auth.get_header();
138
139 let rec = CreateRecord {
140 name: host.to_string(),
141 rtype: RecordType::A,
142 content: ip.to_string(),
143 ttl: 300,
144 };
145 if self.config.dry_run {
146 info!("DRY-RUN: Would have sent {rec:?} to {url}");
147 return Ok(())
148 }
149 http::post::<CreateRecord>(url, &rec, Some(auth)).await?;
150
151 Ok(())
152 }
153
154 async fn update_v4_record(&self, host: &str, ip: &Ipv4Addr) -> Result<()> {
155 let rec = match self.get_record(host, RecordType::A).await? {
156 Some(rec) => rec,
157 None => {
158 warn!("DELETE: Record {host} doesn't exist");
159 return Ok(());
160 }
161 };
162
163 let acc_id = self.get_id().await?;
164 let rid = rec.id;
165
166 let update = UpdateRecord {
167 content: ip.to_string(),
168 };
169
170 let url = format!("{}/{acc_id}/zones/{}/records/{rid}", self.endpoint, self.config.domain)
171 .parse()
172 .map_err(|e| Error::UrlError(format!("Error: {e}")))?;
173 if self.config.dry_run {
174 info!("DRY-RUN: Would have sent PATCH to {url}");
175 return Ok(())
176 }
177
178 let auth = self.auth.get_header();
179 http::patch(url, &update, Some(auth)).await?;
180
181 Ok(())
182 }
183
184 async fn delete_v4_record(&self, host: &str) -> Result<()> {
185 let rec = match self.get_record(host, RecordType::A).await? {
186 Some(rec) => rec,
187 None => {
188 warn!("DELETE: Record {host} doesn't exist");
189 return Ok(());
190 }
191 };
192
193 let acc_id = self.get_id().await?;
194 let rid = rec.id;
195
196 let url = format!("{}/{acc_id}/zones/{}/records/{rid}", self.endpoint, self.config.domain)
197 .parse()
198 .map_err(|e| Error::UrlError(format!("Error: {e}")))?;
199 if self.config.dry_run {
200 info!("DRY-RUN: Would have sent DELETE to {url}");
201 return Ok(())
202 }
203
204 let auth = self.auth.get_header();
205 http::delete(url, Some(auth)).await?;
206
207 Ok(())
208 }
209}
210
211
212
213#[cfg(test)]
214mod tests {
215 use super::*;
216 use std::env;
217 use random_string::charsets::ALPHANUMERIC;
218 use tracing_test::traced_test;
219
220 const TEST_API: &str = "https://api.sandbox.dnsimple.com/v2";
221
222 fn get_client() -> DnSimple {
223 let auth = Auth { key: env::var("DNSIMPLE_TOKEN").unwrap() };
224 let config = Config {
225 domain: env::var("DNSIMPLE_TEST_DOMAIN").unwrap(),
226 dry_run: false,
227 };
228 DnSimple::new_with_endpoint(config, auth, None, TEST_API)
229 }
230
231 async fn test_id_fetch() -> Result<()> {
232 let client = get_client();
233
234 let id = client.get_upstream_id().await?;
235 assert_eq!(2602, id);
236
237 Ok(())
238 }
239
240 async fn test_create_update_delete_ipv4() -> Result<()> {
242 let client = get_client();
243
244 let host = random_string::generate(16, ALPHANUMERIC);
245
246 let ip = "1.1.1.1".parse()?;
248 client.create_v4_record(&host, &ip).await?;
249 let cur = client.get_v4_record(&host).await?;
250 assert_eq!(Some(ip), cur);
251
252
253 let ip = "2.2.2.2".parse()?;
255 client.update_v4_record(&host, &ip).await?;
256 let cur = client.get_v4_record(&host).await?;
257 assert_eq!(Some(ip), cur);
258
259
260 client.delete_v4_record(&host).await?;
262 let del = client.get_v4_record(&host).await?;
263 assert!(del.is_none());
264
265 Ok(())
266 }
267
268
269 #[cfg(feature = "smol")]
270 mod smol {
271 use super::*;
272 use macro_rules_attribute::apply;
273 use smol_macros::test;
274
275 #[apply(test!)]
276 #[traced_test]
277 #[cfg_attr(not(feature = "test_dnsimple"), ignore = "DnSimple API test")]
278 async fn smol_id_fetch() -> Result<()> {
279 test_id_fetch().await
280 }
281
282
283 #[apply(test!)]
284 #[traced_test]
285 #[cfg_attr(not(feature = "test_dnsimple"), ignore = "DnSimple API test")]
286 async fn smol_create_update() -> Result<()> {
287 test_create_update_delete_ipv4().await
288 }
289 }
290
291 #[cfg(feature = "tokio")]
292 mod smol {
293 use super::*;
294
295 #[tokio::test]
296 #[traced_test]
297 #[cfg_attr(not(feature = "test_dnsimple"), ignore = "DnSimple API test")]
298 async fn tokio_id_fetch() -> Result<()> {
299 test_id_fetch().await
300 }
301
302 #[tokio::test]
303 #[traced_test]
304 #[cfg_attr(not(feature = "test_dnsimple"), ignore = "DnSimple API test")]
305 async fn tokio_create_update() -> Result<()> {
306 test_create_update_delete_ipv4().await
307 }
308 }
309
310
311}
312