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};
19
20use serde::{de::DeserializeOwned, Deserialize, Serialize};
21use tracing::warn;
22
23use crate::errors::Result;
24
25
26
27#[derive(Clone, Debug, Deserialize)]
35#[serde(rename_all = "lowercase", tag = "name")]
36pub enum Provider {
37 Gandi(gandi::Auth),
38 Dnsimple(dnsimple::Auth),
39 DnsMadeEasy(dnsmadeeasy::Auth),
40 PorkBun(porkbun::Auth),
41}
42
43pub enum ProviderImpl {
44 Gandi(gandi::Gandi),
45 Dnsimple(dnsimple::Dnsimple),
46 DnsMadeEasy(dnsmadeeasy::DnsMadeEasy),
47 PorkBun(porkbun::Porkbun),
48}
49
50pub struct Config {
55 pub domain: String,
56 pub dry_run: bool,
57}
58
59impl ProviderImpl {
60
61 pub fn new(provider: Provider, conf: Config) -> Self {
62 match provider {
63 #[cfg(feature = "gandi")]
64 Provider::Gandi(auth) => Self::Gandi(gandi::Gandi::new(conf, auth.clone())),
65 #[cfg(feature = "dnsimple")]
66 Provider::Dnsimple(auth) => Self::Dnsimple(dnsimple::Dnsimple::new(conf, auth.clone(), None)),
67 #[cfg(feature = "dnsmadeeasy")]
68 Provider::DnsMadeEasy(auth) => Self::DnsMadeEasy(dnsmadeeasy::DnsMadeEasy::new(conf, auth.clone())),
69 #[cfg(feature = "porkbun")]
70 Provider::PorkBun(auth) => Self::PorkBun(porkbun::Porkbun::new(conf, auth.clone())),
71 }
72 }
73
74 }
91
92impl DnsProvider for ProviderImpl {
93
94 fn get_record<T>(&self, rtype: RecordType, host: &str) -> Result<Option<T>>
95 where T: DeserializeOwned,
96 Self: Sized
97 {
98 match self {
99 Self::Gandi(imp) => imp.get_record(rtype, host),
100 Self::Dnsimple(imp) => imp.get_record(rtype, host),
101 Self::DnsMadeEasy(imp) => imp.get_record(rtype, host),
102 Self::PorkBun(imp) => imp.get_record(rtype, host),
103 }
104 }
105
106
107 fn create_record<T>(&self, rtype: RecordType, host: &str, record: &T) -> Result<()>
109 where T: Serialize + DeserializeOwned + Display + Clone,
110 Self: Sized
111 {
112 match self {
113 Self::Gandi(imp) => imp.create_record(rtype, host, record),
114 Self::Dnsimple(imp) => imp.create_record(rtype, host, record),
115 Self::DnsMadeEasy(imp) => imp.create_record(rtype, host, record),
116 Self::PorkBun(imp) => imp.create_record(rtype, host, record),
117 }
118 }
119
120
121 fn update_record<T>(&self, rtype: RecordType, host: &str, record: &T) -> Result<()>
123 where T: Serialize + DeserializeOwned + Display + Clone,
124 Self: Sized
125 {
126 match self {
127 Self::Gandi(imp) => imp.update_record(rtype, host, record),
128 Self::Dnsimple(imp) => imp.update_record(rtype, host, record),
129 Self::DnsMadeEasy(imp) => imp.update_record(rtype, host, record),
130 Self::PorkBun(imp) => imp.update_record(rtype, host, record),
131 }
132 }
133
134
135 fn delete_record(&self, rtype: RecordType, host: &str) -> Result<()>
137 where Self: Sized
138 {
139 match self {
140 Self::Gandi(imp) => imp.delete_record(rtype, host),
141 Self::Dnsimple(imp) => imp.delete_record(rtype, host),
142 Self::DnsMadeEasy(imp) => imp.delete_record(rtype, host),
143 Self::PorkBun(imp) => imp.delete_record(rtype, host),
144 }
145 }
146}
147
148
149#[derive(Serialize, Deserialize, Clone, Debug)]
150pub enum RecordType {
151 A,
152 AAAA,
153 CAA,
154 CNAME,
155 HINFO,
156 MX,
157 NAPTR,
158 NS,
159 PTR,
160 SRV,
161 SPF,
162 SSHFP,
163 TXT,
164}
165
166impl Display for RecordType {
167 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
168 write!(f, "{:?}", self)
169 }
170}
171
172pub trait DnsProvider {
180 fn get_record<T>(&self, rtype: RecordType, host: &str) -> Result<Option<T>>
182 where T: DeserializeOwned,
183 Self: Sized;
184
185 fn create_record<T>(&self, rtype: RecordType, host: &str, record: &T) -> Result<()>
187 where T: Serialize + DeserializeOwned + Display + Clone,
188 Self: Sized;
189
190 fn update_record<T>(&self, rtype: RecordType, host: &str, record: &T) -> Result<()>
192 where T: Serialize + DeserializeOwned + Display + Clone,
193 Self: Sized;
194
195 fn delete_record(&self, rtype: RecordType, host: &str) -> Result<()>
197 where Self: Sized;
198
199 fn get_txt_record(&self, host: &str) -> Result<Option<String>>
241 where Self: Sized
242 {
243 self.get_record::<String>(RecordType::TXT, host)
244 .map(|opt| opt.map(|s| crate::strip_quotes(&s)))
245 }
246
247 fn create_txt_record(&self, host: &str, record: &String) -> Result<()>
248 where Self: Sized
249 {
250 self.create_record(RecordType::TXT, host, record)
251 }
252
253 fn update_txt_record(&self, host: &str, record: &String) -> Result<()>
254 where Self: Sized
255 {
256 self.update_record(RecordType::TXT, host, record)
257 }
258
259 fn delete_txt_record(&self, host: &str) -> Result<()>
260 where Self: Sized
261 {
262 self.delete_record(RecordType::TXT, host)
263 }
264
265 fn get_a_record(&self, host: &str) -> Result<Option<std::net::Ipv4Addr>>
266 where Self: Sized
267 {
268 self.get_record(RecordType::A, host)
269 }
270
271 fn create_a_record(&self, host: &str, record: &std::net::Ipv4Addr) -> Result<()>
272 where Self: Sized
273 {
274 self.create_record(RecordType::A, host, record)
275 }
276
277 fn update_a_record(&self, host: &str, record: &std::net::Ipv4Addr) -> Result<()>
278 where Self: Sized
279 {
280 self.update_record(RecordType::A, host, record)
281 }
282
283 fn delete_a_record(&self, host: &str) -> Result<()>
284 where Self: Sized
285 {
286 self.delete_record(RecordType::A, host)
287 }
288}
289
290
291#[macro_export]
292macro_rules! generate_helpers {
293 () => {
294
295}
328}
329
330
331fn strip_quotes(record: &str) -> String {
332 let chars = record.chars();
333 let mut check = chars.clone();
334
335 let first = check.next();
336 let last = check.last();
337
338 if let Some('"') = first && let Some('"') = last {
339 chars.skip(1)
340 .take(record.len() - 2)
341 .collect()
342
343 } else {
344 warn!("Double quotes not found in record string, using whole record.");
345 record.to_string()
346 }
347}
348
349
350#[cfg(test)]
351mod tests {
352 use super::*;
353 use std::net::Ipv4Addr;
354 use random_string::charsets::ALPHA_LOWER;
355 use tracing::info;
356
357 #[test]
358 fn test_strip_quotes() -> Result<()> {
359 assert_eq!("abc123".to_string(), strip_quotes("\"abc123\""));
360 assert_eq!("abc123\"", strip_quotes("abc123\""));
361 assert_eq!("\"abc123", strip_quotes("\"abc123"));
362 assert_eq!("abc123", strip_quotes("abc123"));
363
364 Ok(())
365 }
366
367
368 pub(crate) fn test_create_update_delete_ipv4(client: impl DnsProvider) -> Result<()> {
369
370 let host = random_string::generate(16, ALPHA_LOWER);
371
372 info!("Creating IPv4 {host}");
374 let ip: Ipv4Addr = "1.1.1.1".parse()?;
375 client.create_record(RecordType::A, &host, &ip)?;
376 let cur = client.get_record(RecordType::A, &host)?;
377 assert_eq!(Some(ip), cur);
378
379
380 info!("Updating IPv4 {host}");
382 let ip: Ipv4Addr = "2.2.2.2".parse()?;
383 client.update_record(RecordType::A, &host, &ip)?;
384 let cur = client.get_record(RecordType::A, &host)?;
385 assert_eq!(Some(ip), cur);
386
387
388 info!("Deleting IPv4 {host}");
390 client.delete_record(RecordType::A, &host)?;
391 let del: Option<Ipv4Addr> = client.get_record(RecordType::A, &host)?;
392 assert!(del.is_none());
393
394 Ok(())
395 }
396
397 pub(crate) fn test_create_update_delete_txt(client: impl DnsProvider) -> Result<()> {
398
399 let host = random_string::generate(16, ALPHA_LOWER);
400
401 let txt = "a text reference".to_string();
403 client.create_record(RecordType::TXT, &host, &txt)?;
404 let cur: Option<String> = client.get_record(RecordType::TXT, &host)?;
405 assert_eq!(txt, strip_quotes(&cur.unwrap()));
406
407
408 let txt = "another text reference".to_string();
410 client.update_record(RecordType::TXT, &host, &txt)?;
411 let cur: Option<String> = client.get_record(RecordType::TXT, &host)?;
412 assert_eq!(txt, strip_quotes(&cur.unwrap()));
413
414
415 client.delete_record(RecordType::TXT, &host)?;
417 let del: Option<String> = client.get_record(RecordType::TXT, &host)?;
418 assert!(del.is_none());
419
420 Ok(())
421 }
422
423 pub(crate) fn test_create_update_delete_txt_default(client: impl DnsProvider) -> Result<()> {
424
425 let host = random_string::generate(16, ALPHA_LOWER);
426
427 let txt = "a text reference".to_string();
429 client.create_txt_record(&host, &txt)?;
430 let cur = client.get_txt_record(&host)?;
431 assert_eq!(txt, strip_quotes(&cur.unwrap()));
432
433
434 let txt = "another text reference".to_string();
436 client.update_txt_record(&host, &txt)?;
437 let cur = client.get_txt_record(&host)?;
438 assert_eq!(txt, strip_quotes(&cur.unwrap()));
439
440
441 client.delete_txt_record(&host)?;
443 let del = client.get_txt_record(&host)?;
444 assert!(del.is_none());
445
446 Ok(())
447 }
448
449 #[macro_export]
482 macro_rules! generate_tests {
483 ($feat:literal) => {
484
485 #[test_log::test]
486 #[cfg_attr(not(feature = $feat), ignore = "API test")]
487 fn create_update_v4() -> Result<()> {
488 test_create_update_delete_ipv4(get_client())?;
489 Ok(())
490 }
491
492 #[test_log::test]
493 #[cfg_attr(not(feature = $feat), ignore = "API test")]
494 fn create_update_txt() -> Result<()> {
495 test_create_update_delete_txt(get_client())?;
496 Ok(())
497 }
498
499 #[test_log::test]
500 #[cfg_attr(not(feature = $feat), ignore = "API test")]
501 fn create_update_default() -> Result<()> {
502 test_create_update_delete_txt_default(get_client())?;
503 Ok(())
504 }
505 }
506 }
507
508
509}