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(Serialize, Deserialize, Clone, Debug)]
32pub enum RecordType {
33    A,
34    AAAA,
35    CAA,
36    CNAME,
37    HINFO,
38    MX,
39    NAPTR,
40    NS,
41    PTR,
42    SRV,
43    SPF,
44    SSHFP,
45    TXT,
46}
47
48impl Display for RecordType {
49    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
50        write!(f, "{:?}", self)
51    }
52}
53
54pub trait DnsProvider {
62    fn get_record<T>(&self, rtype: RecordType, host: &str) -> Result<Option<T>>
64    where T: DeserializeOwned,
65          Self: Sized;
66
67    fn create_record<T>(&self, rtype: RecordType, host: &str, record: &T) -> Result<()>
69    where T: Serialize + DeserializeOwned + Display + Clone,
70          Self: Sized;
71
72    fn update_record<T>(&self, rtype: RecordType, host: &str, record: &T) -> Result<()>
74    where T: Serialize + DeserializeOwned + Display + Clone,
75          Self: Sized;
76
77    fn delete_record(&self, rtype: RecordType, host: &str) -> Result<()>
79    where Self: Sized;
80
81    fn get_txt_record(&self, host: &str) -> Result<Option<String>>;
85
86    fn create_txt_record(&self, host: &str, record: &String) -> Result<()>;
90
91    fn update_txt_record(&self, host: &str, record: &String) -> Result<()>;
95
96    fn delete_txt_record(&self, host: &str) -> Result<()>;
100
101    fn get_a_record(&self, host: &str) -> Result<Option<Ipv4Addr>>;
105
106    fn create_a_record(&self, host: &str, record: &Ipv4Addr) -> Result<()>;
110
111    fn update_a_record(&self, host: &str, record: &Ipv4Addr) -> Result<()>;
115
116    fn delete_a_record(&self, host: &str) -> Result<()>;
120}
121
122
123#[macro_export]
124macro_rules! generate_helpers {
125    () => {
126
127        fn get_txt_record(&self, host: &str) -> Result<Option<String>> {
128            self.get_record::<String>(RecordType::TXT, host)
129                .map(|opt| opt.map(|s| crate::strip_quotes(&s)))
130        }
131
132        fn create_txt_record(&self, host: &str, record: &String) -> Result<()> {
133            self.create_record(RecordType::TXT, host, record)
134        }
135
136        fn update_txt_record(&self, host: &str, record: &String) -> Result<()> {
137            self.update_record(RecordType::TXT, host, record)
138        }
139
140        fn delete_txt_record(&self, host: &str) -> Result<()> {
141            self.delete_record(RecordType::TXT, host)
142        }
143
144        fn get_a_record(&self, host: &str) -> Result<Option<std::net::Ipv4Addr>> {
145            self.get_record(RecordType::A, host)
146        }
147
148        fn create_a_record(&self, host: &str, record: &std::net::Ipv4Addr) -> Result<()> {
149            self.create_record(RecordType::A, host, record)
150        }
151
152        fn update_a_record(&self, host: &str, record: &std::net::Ipv4Addr) -> Result<()> {
153            self.update_record(RecordType::A, host, record)
154        }
155
156        fn delete_a_record(&self, host: &str) -> Result<()> {
157            self.delete_record(RecordType::A, host)
158        }
159    }
160}
161
162
163fn strip_quotes(record: &str) -> String {
164    let chars = record.chars();
165    let mut check = chars.clone();
166
167    let first = check.next();
168    let last = check.last();
169
170    if let Some('"') = first && let Some('"') = last {
171        chars.skip(1)
172            .take(record.len() - 2)
173            .collect()
174
175    } else {
176        warn!("Double quotes not found in record string, using whole record.");
177        record.to_string()
178    }
179}
180
181
182#[cfg(test)]
183mod tests {
184    use super::*;
185    use std::net::Ipv4Addr;
186    use random_string::charsets::ALPHA_LOWER;
187    use tracing::info;
188
189    #[test]
190    fn test_strip_quotes() -> Result<()> {
191        assert_eq!("abc123".to_string(), strip_quotes("\"abc123\""));
192        assert_eq!("abc123\"", strip_quotes("abc123\""));
193        assert_eq!("\"abc123", strip_quotes("\"abc123"));
194        assert_eq!("abc123", strip_quotes("abc123"));
195
196        Ok(())
197    }
198
199
200    pub(crate) fn test_create_update_delete_ipv4(client: impl DnsProvider) -> Result<()> {
201
202        let host = random_string::generate(16, ALPHA_LOWER);
203
204        info!("Creating IPv4 {host}");
206        let ip: Ipv4Addr = "1.1.1.1".parse()?;
207        client.create_record(RecordType::A, &host, &ip)?;
208        let cur = client.get_record(RecordType::A, &host)?;
209        assert_eq!(Some(ip), cur);
210
211
212        info!("Updating IPv4 {host}");
214        let ip: Ipv4Addr = "2.2.2.2".parse()?;
215        client.update_record(RecordType::A, &host, &ip)?;
216        let cur = client.get_record(RecordType::A, &host)?;
217        assert_eq!(Some(ip), cur);
218
219
220        info!("Deleting IPv4 {host}");
222        client.delete_record(RecordType::A, &host)?;
223        let del: Option<Ipv4Addr> = client.get_record(RecordType::A, &host)?;
224        assert!(del.is_none());
225
226        Ok(())
227    }
228
229    pub(crate) fn test_create_update_delete_txt(client: impl DnsProvider) -> Result<()> {
230
231        let host = random_string::generate(16, ALPHA_LOWER);
232
233        let txt = "a text reference".to_string();
235        client.create_record(RecordType::TXT, &host, &txt)?;
236        let cur: Option<String> = client.get_record(RecordType::TXT, &host)?;
237        assert_eq!(txt, strip_quotes(&cur.unwrap()));
238
239
240        let txt = "another text reference".to_string();
242        client.update_record(RecordType::TXT, &host, &txt)?;
243        let cur: Option<String> = client.get_record(RecordType::TXT, &host)?;
244        assert_eq!(txt, strip_quotes(&cur.unwrap()));
245
246
247        client.delete_record(RecordType::TXT, &host)?;
249        let del: Option<String> = client.get_record(RecordType::TXT, &host)?;
250        assert!(del.is_none());
251
252        Ok(())
253    }
254
255    pub(crate) fn test_create_update_delete_txt_default(client: impl DnsProvider) -> Result<()> {
256
257        let host = random_string::generate(16, ALPHA_LOWER);
258
259        let txt = "a text reference".to_string();
261        client.create_txt_record(&host, &txt)?;
262        let cur = client.get_txt_record(&host)?;
263        assert_eq!(txt, strip_quotes(&cur.unwrap()));
264
265
266        let txt = "another text reference".to_string();
268        client.update_txt_record(&host, &txt)?;
269        let cur = client.get_txt_record(&host)?;
270        assert_eq!(txt, strip_quotes(&cur.unwrap()));
271
272
273        client.delete_txt_record(&host)?;
275        let del = client.get_txt_record(&host)?;
276        assert!(del.is_none());
277
278        Ok(())
279    }
280
281    #[macro_export]
314    macro_rules! generate_tests {
315        ($feat:literal) => {
316
317            #[test_log::test]
318            #[cfg_attr(not(feature = $feat), ignore = "API test")]
319            fn create_update_v4() -> Result<()> {
320                test_create_update_delete_ipv4(get_client())?;
321                Ok(())
322            }
323
324            #[test_log::test]
325            #[cfg_attr(not(feature = $feat), ignore = "API test")]
326            fn create_update_txt() -> Result<()> {
327                test_create_update_delete_txt(get_client())?;
328                Ok(())
329            }
330
331            #[test_log::test]
332            #[cfg_attr(not(feature = $feat), ignore = "API test")]
333            fn create_update_default() -> Result<()> {
334                test_create_update_delete_txt_default(get_client())?;
335                Ok(())
336            }
337        }
338    }
339
340
341}