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 {
27 pub domain: String,
28 pub dry_run: bool,
29}
30
31#[derive(Clone, Debug, Deserialize)]
35#[serde(rename_all = "lowercase", tag = "name")]
36pub enum Providers {
37 Gandi(gandi::Auth),
38 Dnsimple(dnsimple::Auth),
39 DnsMadeEasy(dnsmadeeasy::Auth),
40 PorkBun(porkbun::Auth),
41}
42
43impl Providers {
44
45 pub fn blocking_impl(&self, dns_conf: Config) -> Box<dyn DnsProvider> {
46 match self {
47 #[cfg(feature = "gandi")]
48 Providers::Gandi(auth) => Box::new(gandi::Gandi::new(dns_conf, auth.clone())),
49 #[cfg(feature = "dnsimple")]
50 Providers::Dnsimple(auth) => Box::new(dnsimple::Dnsimple::new(dns_conf, auth.clone(), None)),
51 #[cfg(feature = "dnsmadeeasy")]
52 Providers::DnsMadeEasy(auth) => Box::new(dnsmadeeasy::DnsMadeEasy::new(dns_conf, auth.clone())),
53 #[cfg(feature = "porkbun")]
54 Providers::PorkBun(auth) => Box::new(porkbun::Porkbun::new(dns_conf, auth.clone())),
55 }
56 }
57
58 #[cfg(feature = "async")]
59 pub fn async_impl(&self, dns_conf: Config) -> Box<dyn async_impl::AsyncDnsProvider> {
60 match self {
61 #[cfg(feature = "gandi")]
62 Providers::Gandi(auth) => Box::new(async_impl::gandi::Gandi::new(dns_conf, auth.clone())),
63 #[cfg(feature = "dnsimple")]
64 Providers::Dnsimple(auth) => Box::new(async_impl::dnsimple::Dnsimple::new(dns_conf, auth.clone(), None)),
65 #[cfg(feature = "dnsmadeeasy")]
66 Providers::DnsMadeEasy(auth) => Box::new(async_impl::dnsmadeeasy::DnsMadeEasy::new(dns_conf, auth.clone())),
67 #[cfg(feature = "porkbun")]
68 Providers::PorkBun(auth) => Box::new(async_impl::porkbun::Porkbun::new(dns_conf, auth.clone())),
69 }
70 }
71}
72
73
74
75#[derive(Serialize, Deserialize, Clone, Debug)]
76pub enum RecordType {
77 A,
78 AAAA,
79 CAA,
80 CNAME,
81 HINFO,
82 MX,
83 NAPTR,
84 NS,
85 PTR,
86 SRV,
87 SPF,
88 SSHFP,
89 TXT,
90}
91
92impl Display for RecordType {
93 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
94 write!(f, "{:?}", self)
95 }
96}
97
98pub trait DnsProvider {
106 fn get_record<T>(&self, rtype: RecordType, host: &str) -> Result<Option<T>>
108 where T: DeserializeOwned,
109 Self: Sized;
110
111 fn create_record<T>(&self, rtype: RecordType, host: &str, record: &T) -> Result<()>
113 where T: Serialize + DeserializeOwned + Display + Clone,
114 Self: Sized;
115
116 fn update_record<T>(&self, rtype: RecordType, host: &str, record: &T) -> Result<()>
118 where T: Serialize + DeserializeOwned + Display + Clone,
119 Self: Sized;
120
121 fn delete_record(&self, rtype: RecordType, host: &str) -> Result<()>
123 where Self: Sized;
124
125 fn get_txt_record(&self, host: &str) -> Result<Option<String>>;
129
130 fn create_txt_record(&self, host: &str, record: &String) -> Result<()>;
134
135 fn update_txt_record(&self, host: &str, record: &String) -> Result<()>;
139
140 fn delete_txt_record(&self, host: &str) -> Result<()>;
144
145 fn get_a_record(&self, host: &str) -> Result<Option<Ipv4Addr>>;
149
150 fn create_a_record(&self, host: &str, record: &Ipv4Addr) -> Result<()>;
154
155 fn update_a_record(&self, host: &str, record: &Ipv4Addr) -> Result<()>;
159
160 fn delete_a_record(&self, host: &str) -> Result<()>;
164}
165
166
167#[macro_export]
168macro_rules! generate_helpers {
169 () => {
170
171 fn get_txt_record(&self, host: &str) -> Result<Option<String>> {
172 self.get_record::<String>(RecordType::TXT, host)
173 .map(|opt| opt.map(|s| crate::strip_quotes(&s)))
174 }
175
176 fn create_txt_record(&self, host: &str, record: &String) -> Result<()> {
177 self.create_record(RecordType::TXT, host, record)
178 }
179
180 fn update_txt_record(&self, host: &str, record: &String) -> Result<()> {
181 self.update_record(RecordType::TXT, host, record)
182 }
183
184 fn delete_txt_record(&self, host: &str) -> Result<()> {
185 self.delete_record(RecordType::TXT, host)
186 }
187
188 fn get_a_record(&self, host: &str) -> Result<Option<std::net::Ipv4Addr>> {
189 self.get_record(RecordType::A, host)
190 }
191
192 fn create_a_record(&self, host: &str, record: &std::net::Ipv4Addr) -> Result<()> {
193 self.create_record(RecordType::A, host, record)
194 }
195
196 fn update_a_record(&self, host: &str, record: &std::net::Ipv4Addr) -> Result<()> {
197 self.update_record(RecordType::A, host, record)
198 }
199
200 fn delete_a_record(&self, host: &str) -> Result<()> {
201 self.delete_record(RecordType::A, host)
202 }
203 }
204}
205
206
207fn strip_quotes(record: &str) -> String {
208 let chars = record.chars();
209 let mut check = chars.clone();
210
211 let first = check.next();
212 let last = check.last();
213
214 if let Some('"') = first && let Some('"') = last {
215 chars.skip(1)
216 .take(record.len() - 2)
217 .collect()
218
219 } else {
220 warn!("Double quotes not found in record string, using whole record.");
221 record.to_string()
222 }
223}
224
225
226#[cfg(test)]
227mod tests {
228 use super::*;
229 use std::net::Ipv4Addr;
230 use random_string::charsets::ALPHA_LOWER;
231 use tracing::info;
232
233 #[test]
234 fn test_strip_quotes() -> Result<()> {
235 assert_eq!("abc123".to_string(), strip_quotes("\"abc123\""));
236 assert_eq!("abc123\"", strip_quotes("abc123\""));
237 assert_eq!("\"abc123", strip_quotes("\"abc123"));
238 assert_eq!("abc123", strip_quotes("abc123"));
239
240 Ok(())
241 }
242
243
244 pub(crate) fn test_create_update_delete_ipv4(client: impl DnsProvider) -> Result<()> {
245
246 let host = random_string::generate(16, ALPHA_LOWER);
247
248 info!("Creating IPv4 {host}");
250 let ip: Ipv4Addr = "1.1.1.1".parse()?;
251 client.create_record(RecordType::A, &host, &ip)?;
252 let cur = client.get_record(RecordType::A, &host)?;
253 assert_eq!(Some(ip), cur);
254
255
256 info!("Updating IPv4 {host}");
258 let ip: Ipv4Addr = "2.2.2.2".parse()?;
259 client.update_record(RecordType::A, &host, &ip)?;
260 let cur = client.get_record(RecordType::A, &host)?;
261 assert_eq!(Some(ip), cur);
262
263
264 info!("Deleting IPv4 {host}");
266 client.delete_record(RecordType::A, &host)?;
267 let del: Option<Ipv4Addr> = client.get_record(RecordType::A, &host)?;
268 assert!(del.is_none());
269
270 Ok(())
271 }
272
273 pub(crate) fn test_create_update_delete_txt(client: impl DnsProvider) -> Result<()> {
274
275 let host = random_string::generate(16, ALPHA_LOWER);
276
277 let txt = "a text reference".to_string();
279 client.create_record(RecordType::TXT, &host, &txt)?;
280 let cur: Option<String> = client.get_record(RecordType::TXT, &host)?;
281 assert_eq!(txt, strip_quotes(&cur.unwrap()));
282
283
284 let txt = "another text reference".to_string();
286 client.update_record(RecordType::TXT, &host, &txt)?;
287 let cur: Option<String> = client.get_record(RecordType::TXT, &host)?;
288 assert_eq!(txt, strip_quotes(&cur.unwrap()));
289
290
291 client.delete_record(RecordType::TXT, &host)?;
293 let del: Option<String> = client.get_record(RecordType::TXT, &host)?;
294 assert!(del.is_none());
295
296 Ok(())
297 }
298
299 pub(crate) fn test_create_update_delete_txt_default(client: impl DnsProvider) -> Result<()> {
300
301 let host = random_string::generate(16, ALPHA_LOWER);
302
303 let txt = "a text reference".to_string();
305 client.create_txt_record(&host, &txt)?;
306 let cur = client.get_txt_record(&host)?;
307 assert_eq!(txt, strip_quotes(&cur.unwrap()));
308
309
310 let txt = "another text reference".to_string();
312 client.update_txt_record(&host, &txt)?;
313 let cur = client.get_txt_record(&host)?;
314 assert_eq!(txt, strip_quotes(&cur.unwrap()));
315
316
317 client.delete_txt_record(&host)?;
319 let del = client.get_txt_record(&host)?;
320 assert!(del.is_none());
321
322 Ok(())
323 }
324
325 #[macro_export]
358 macro_rules! generate_tests {
359 ($feat:literal) => {
360
361 #[test_log::test]
362 #[cfg_attr(not(feature = $feat), ignore = "API test")]
363 fn create_update_v4() -> Result<()> {
364 test_create_update_delete_ipv4(get_client())?;
365 Ok(())
366 }
367
368 #[test_log::test]
369 #[cfg_attr(not(feature = $feat), ignore = "API test")]
370 fn create_update_txt() -> Result<()> {
371 test_create_update_delete_txt(get_client())?;
372 Ok(())
373 }
374
375 #[test_log::test]
376 #[cfg_attr(not(feature = $feat), ignore = "API test")]
377 fn create_update_default() -> Result<()> {
378 test_create_update_delete_txt_default(get_client())?;
379 Ok(())
380 }
381 }
382 }
383
384
385}