1
2
3pub mod errors;
4mod http;
5
6#[cfg(feature = "async")]
7pub mod async_impl;
8
9#[cfg(feature = "dnsimple")]
10pub mod dnsimple;
11#[cfg(feature = "dnsmadeeasy")]
12pub mod dnsmadeeasy;
13#[cfg(feature = "gandi")]
14pub mod gandi;
15#[cfg(feature = "porkbun")]
16pub mod porkbun;
17
18use std::{fmt::{self, Debug, Display, Formatter}, net::Ipv4Addr};
19
20use serde::{de::DeserializeOwned, Deserialize, Serialize};
21use tracing::warn;
22
23use crate::errors::Result;
24
25
26pub struct Config {
31 pub domain: String,
32 pub dry_run: bool,
33}
34
35#[derive(Clone, Debug, Deserialize)]
43#[serde(rename_all = "lowercase", tag = "name")]
44pub enum Provider {
45 Gandi(gandi::Auth),
46 Dnsimple(dnsimple::Auth),
47 DnsMadeEasy(dnsmadeeasy::Auth),
48 PorkBun(porkbun::Auth),
49}
50
51impl Provider {
52
53 pub fn blocking_impl(&self, dns_conf: Config) -> Box<dyn DnsProvider> {
57 match self {
58 #[cfg(feature = "gandi")]
59 Provider::Gandi(auth) => Box::new(gandi::Gandi::new(dns_conf, auth.clone())),
60 #[cfg(feature = "dnsimple")]
61 Provider::Dnsimple(auth) => Box::new(dnsimple::Dnsimple::new(dns_conf, auth.clone(), None)),
62 #[cfg(feature = "dnsmadeeasy")]
63 Provider::DnsMadeEasy(auth) => Box::new(dnsmadeeasy::DnsMadeEasy::new(dns_conf, auth.clone())),
64 #[cfg(feature = "porkbun")]
65 Provider::PorkBun(auth) => Box::new(porkbun::Porkbun::new(dns_conf, auth.clone())),
66 }
67 }
68
69 #[cfg(feature = "async")]
73 pub fn async_impl(&self, dns_conf: Config) -> Box<dyn async_impl::AsyncDnsProvider> {
74 match self {
75 #[cfg(feature = "gandi")]
76 Provider::Gandi(auth) => Box::new(async_impl::gandi::Gandi::new(dns_conf, auth.clone())),
77 #[cfg(feature = "dnsimple")]
78 Provider::Dnsimple(auth) => Box::new(async_impl::dnsimple::Dnsimple::new(dns_conf, auth.clone(), None)),
79 #[cfg(feature = "dnsmadeeasy")]
80 Provider::DnsMadeEasy(auth) => Box::new(async_impl::dnsmadeeasy::DnsMadeEasy::new(dns_conf, auth.clone())),
81 #[cfg(feature = "porkbun")]
82 Provider::PorkBun(auth) => Box::new(async_impl::porkbun::Porkbun::new(dns_conf, auth.clone())),
83 }
84 }
85}
86
87
88
89#[derive(Serialize, Deserialize, Clone, Debug)]
90pub enum RecordType {
91 A,
92 AAAA,
93 CAA,
94 CNAME,
95 HINFO,
96 MX,
97 NAPTR,
98 NS,
99 PTR,
100 SRV,
101 SPF,
102 SSHFP,
103 TXT,
104}
105
106impl Display for RecordType {
107 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
108 write!(f, "{:?}", self)
109 }
110}
111
112pub trait DnsProvider {
120 fn get_record<T>(&self, rtype: RecordType, host: &str) -> Result<Option<T>>
122 where T: DeserializeOwned,
123 Self: Sized;
124
125 fn create_record<T>(&self, rtype: RecordType, host: &str, record: &T) -> Result<()>
127 where T: Serialize + DeserializeOwned + Display + Clone,
128 Self: Sized;
129
130 fn update_record<T>(&self, rtype: RecordType, host: &str, record: &T) -> Result<()>
132 where T: Serialize + DeserializeOwned + Display + Clone,
133 Self: Sized;
134
135 fn delete_record(&self, rtype: RecordType, host: &str) -> Result<()>
137 where Self: Sized;
138
139 fn get_txt_record(&self, host: &str) -> Result<Option<String>>;
143
144 fn create_txt_record(&self, host: &str, record: &String) -> Result<()>;
148
149 fn update_txt_record(&self, host: &str, record: &String) -> Result<()>;
153
154 fn delete_txt_record(&self, host: &str) -> Result<()>;
158
159 fn get_a_record(&self, host: &str) -> Result<Option<Ipv4Addr>>;
163
164 fn create_a_record(&self, host: &str, record: &Ipv4Addr) -> Result<()>;
168
169 fn update_a_record(&self, host: &str, record: &Ipv4Addr) -> Result<()>;
173
174 fn delete_a_record(&self, host: &str) -> Result<()>;
178}
179
180#[macro_export]
190macro_rules! generate_helpers {
191 () => {
192
193 fn get_txt_record(&self, host: &str) -> Result<Option<String>> {
194 self.get_record::<String>(RecordType::TXT, host)
195 .map(|opt| opt.map(|s| crate::strip_quotes(&s)))
196 }
197
198 fn create_txt_record(&self, host: &str, record: &String) -> Result<()> {
199 self.create_record(RecordType::TXT, host, record)
200 }
201
202 fn update_txt_record(&self, host: &str, record: &String) -> Result<()> {
203 self.update_record(RecordType::TXT, host, record)
204 }
205
206 fn delete_txt_record(&self, host: &str) -> Result<()> {
207 self.delete_record(RecordType::TXT, host)
208 }
209
210 fn get_a_record(&self, host: &str) -> Result<Option<std::net::Ipv4Addr>> {
211 self.get_record(RecordType::A, host)
212 }
213
214 fn create_a_record(&self, host: &str, record: &std::net::Ipv4Addr) -> Result<()> {
215 self.create_record(RecordType::A, host, record)
216 }
217
218 fn update_a_record(&self, host: &str, record: &std::net::Ipv4Addr) -> Result<()> {
219 self.update_record(RecordType::A, host, record)
220 }
221
222 fn delete_a_record(&self, host: &str) -> Result<()> {
223 self.delete_record(RecordType::A, host)
224 }
225 }
226}
227
228
229fn strip_quotes(record: &str) -> String {
230 let chars = record.chars();
231 let mut check = chars.clone();
232
233 let first = check.next();
234 let last = check.last();
235
236 if let Some('"') = first && let Some('"') = last {
237 chars.skip(1)
238 .take(record.len() - 2)
239 .collect()
240
241 } else {
242 warn!("Double quotes not found in record string, using whole record.");
243 record.to_string()
244 }
245}
246
247
248#[cfg(test)]
249mod tests {
250 use super::*;
251 use std::net::Ipv4Addr;
252 use random_string::charsets::ALPHA_LOWER;
253 use tracing::info;
254
255 #[test]
256 fn test_strip_quotes() -> Result<()> {
257 assert_eq!("abc123".to_string(), strip_quotes("\"abc123\""));
258 assert_eq!("abc123\"", strip_quotes("abc123\""));
259 assert_eq!("\"abc123", strip_quotes("\"abc123"));
260 assert_eq!("abc123", strip_quotes("abc123"));
261
262 Ok(())
263 }
264
265
266 pub(crate) fn test_create_update_delete_ipv4(client: impl DnsProvider) -> Result<()> {
267
268 let host = random_string::generate(16, ALPHA_LOWER);
269
270 info!("Creating IPv4 {host}");
272 let ip: Ipv4Addr = "1.1.1.1".parse()?;
273 client.create_record(RecordType::A, &host, &ip)?;
274 let cur = client.get_record(RecordType::A, &host)?;
275 assert_eq!(Some(ip), cur);
276
277
278 info!("Updating IPv4 {host}");
280 let ip: Ipv4Addr = "2.2.2.2".parse()?;
281 client.update_record(RecordType::A, &host, &ip)?;
282 let cur = client.get_record(RecordType::A, &host)?;
283 assert_eq!(Some(ip), cur);
284
285
286 info!("Deleting IPv4 {host}");
288 client.delete_record(RecordType::A, &host)?;
289 let del: Option<Ipv4Addr> = client.get_record(RecordType::A, &host)?;
290 assert!(del.is_none());
291
292 Ok(())
293 }
294
295 pub(crate) fn test_create_update_delete_txt(client: impl DnsProvider) -> Result<()> {
296
297 let host = random_string::generate(16, ALPHA_LOWER);
298
299 let txt = "a text reference".to_string();
301 client.create_record(RecordType::TXT, &host, &txt)?;
302 let cur: Option<String> = client.get_record(RecordType::TXT, &host)?;
303 assert_eq!(txt, strip_quotes(&cur.unwrap()));
304
305
306 let txt = "another text reference".to_string();
308 client.update_record(RecordType::TXT, &host, &txt)?;
309 let cur: Option<String> = client.get_record(RecordType::TXT, &host)?;
310 assert_eq!(txt, strip_quotes(&cur.unwrap()));
311
312
313 client.delete_record(RecordType::TXT, &host)?;
315 let del: Option<String> = client.get_record(RecordType::TXT, &host)?;
316 assert!(del.is_none());
317
318 Ok(())
319 }
320
321 pub(crate) fn test_create_update_delete_txt_default(client: impl DnsProvider) -> Result<()> {
322
323 let host = random_string::generate(16, ALPHA_LOWER);
324
325 let txt = "a text reference".to_string();
327 client.create_txt_record(&host, &txt)?;
328 let cur = client.get_txt_record(&host)?;
329 assert_eq!(txt, strip_quotes(&cur.unwrap()));
330
331
332 let txt = "another text reference".to_string();
334 client.update_txt_record(&host, &txt)?;
335 let cur = client.get_txt_record(&host)?;
336 assert_eq!(txt, strip_quotes(&cur.unwrap()));
337
338
339 client.delete_txt_record(&host)?;
341 let del = client.get_txt_record(&host)?;
342 assert!(del.is_none());
343
344 Ok(())
345 }
346
347 #[macro_export]
380 macro_rules! generate_tests {
381 ($feat:literal) => {
382
383 #[test_log::test]
384 #[cfg_attr(not(feature = $feat), ignore = "API test")]
385 fn create_update_v4() -> Result<()> {
386 test_create_update_delete_ipv4(get_client())?;
387 Ok(())
388 }
389
390 #[test_log::test]
391 #[cfg_attr(not(feature = $feat), ignore = "API test")]
392 fn create_update_txt() -> Result<()> {
393 test_create_update_delete_txt(get_client())?;
394 Ok(())
395 }
396
397 #[test_log::test]
398 #[cfg_attr(not(feature = $feat), ignore = "API test")]
399 fn create_update_default() -> Result<()> {
400 test_create_update_delete_txt_default(get_client())?;
401 Ok(())
402 }
403 }
404 }
405
406
407}