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
54#[allow(unused)]
55pub trait DnsProvider {
56 fn get_record<T>(&self, rtype: RecordType, host: &str) -> Result<Option<T>>
57 where
58 T: DeserializeOwned;
59
60 fn create_record<T>(&self, rtype: RecordType, host: &str, record: &T) -> Result<()>
61 where
62 T: Serialize + DeserializeOwned + Display + Clone;
63
64 fn update_record<T>(&self, rtype: RecordType, host: &str, record: &T) -> Result<()>
65 where
66 T: Serialize + DeserializeOwned + Display + Clone;
67
68 fn delete_record(&self, rtype: RecordType, host: &str) -> Result<()>;
69
70
71 fn get_txt_record(&self, host: &str) -> Result<Option<String>> {
74 self.get_record::<String>(RecordType::TXT, host)
75 .map(|opt| opt.map(|s| strip_quotes(&s)))
76 }
77
78 fn create_txt_record(&self, host: &str, record: &String) -> Result<()> {
79 self.create_record(RecordType::TXT, host, record)
80 }
81
82 fn update_txt_record(&self, host: &str, record: &String) -> Result<()> {
83 self.update_record(RecordType::TXT, host, record)
84 }
85
86 fn delete_txt_record(&self, host: &str) -> Result<()> {
87 self.delete_record(RecordType::TXT, host)
88 }
89
90 fn get_a_record(&self, host: &str) -> Result<Option<Ipv4Addr>> {
91 self.get_record(RecordType::A, host)
92 }
93
94 fn create_a_record(&self, host: &str, record: &Ipv4Addr) -> Result<()> {
95 self.create_record(RecordType::A, host, record)
96 }
97
98 fn update_a_record(&self, host: &str, record: &Ipv4Addr) -> Result<()> {
99 self.update_record(RecordType::A, host, record)
100 }
101
102 fn delete_a_record(&self, host: &str) -> Result<()> {
103 self.delete_record(RecordType::A, host)
104 }
105}
106
107
108fn strip_quotes(record: &str) -> String {
109 let chars = record.chars();
110 let mut check = chars.clone();
111
112 let first = check.next();
113 let last = check.last();
114
115 if let Some('"') = first && let Some('"') = last {
116 chars.skip(1)
117 .take(record.len() - 2)
118 .collect()
119
120 } else {
121 warn!("Double quotes not found in record string, using whole record.");
122 record.to_string()
123 }
124}
125
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130 use std::net::Ipv4Addr;
131 use random_string::charsets::ALPHA_LOWER;
132 use tracing::info;
133
134 #[test]
135 fn test_strip_quotes() -> Result<()> {
136 assert_eq!("abc123".to_string(), strip_quotes("\"abc123\""));
137 assert_eq!("abc123\"", strip_quotes("abc123\""));
138 assert_eq!("\"abc123", strip_quotes("\"abc123"));
139 assert_eq!("abc123", strip_quotes("abc123"));
140
141 Ok(())
142 }
143
144
145 pub(crate) fn test_create_update_delete_ipv4(client: impl DnsProvider) -> Result<()> {
146
147 let host = random_string::generate(16, ALPHA_LOWER);
148
149 info!("Creating IPv4 {host}");
151 let ip: Ipv4Addr = "1.1.1.1".parse()?;
152 client.create_record(RecordType::A, &host, &ip)?;
153 let cur = client.get_record(RecordType::A, &host)?;
154 assert_eq!(Some(ip), cur);
155
156
157 info!("Updating IPv4 {host}");
159 let ip: Ipv4Addr = "2.2.2.2".parse()?;
160 client.update_record(RecordType::A, &host, &ip)?;
161 let cur = client.get_record(RecordType::A, &host)?;
162 assert_eq!(Some(ip), cur);
163
164
165 info!("Deleting IPv4 {host}");
167 client.delete_record(RecordType::A, &host)?;
168 let del: Option<Ipv4Addr> = client.get_record(RecordType::A, &host)?;
169 assert!(del.is_none());
170
171 Ok(())
172 }
173
174 pub(crate) fn test_create_update_delete_txt(client: impl DnsProvider) -> Result<()> {
175
176 let host = random_string::generate(16, ALPHA_LOWER);
177
178 let txt = "a text reference".to_string();
180 client.create_record(RecordType::TXT, &host, &txt)?;
181 let cur: Option<String> = client.get_record(RecordType::TXT, &host)?;
182 assert_eq!(txt, strip_quotes(&cur.unwrap()));
183
184
185 let txt = "another text reference".to_string();
187 client.update_record(RecordType::TXT, &host, &txt)?;
188 let cur: Option<String> = client.get_record(RecordType::TXT, &host)?;
189 assert_eq!(txt, strip_quotes(&cur.unwrap()));
190
191
192 client.delete_record(RecordType::TXT, &host)?;
194 let del: Option<String> = client.get_record(RecordType::TXT, &host)?;
195 assert!(del.is_none());
196
197 Ok(())
198 }
199
200 pub(crate) fn test_create_update_delete_txt_default(client: impl DnsProvider) -> Result<()> {
201
202 let host = random_string::generate(16, ALPHA_LOWER);
203
204 let txt = "a text reference".to_string();
206 client.create_txt_record(&host, &txt)?;
207 let cur = client.get_txt_record(&host)?;
208 assert_eq!(txt, strip_quotes(&cur.unwrap()));
209
210
211 let txt = "another text reference".to_string();
213 client.update_txt_record(&host, &txt)?;
214 let cur = client.get_txt_record(&host)?;
215 assert_eq!(txt, strip_quotes(&cur.unwrap()));
216
217
218 client.delete_txt_record(&host)?;
220 let del = client.get_txt_record(&host)?;
221 assert!(del.is_none());
222
223 Ok(())
224 }
225
226 #[macro_export]
259 macro_rules! generate_tests {
260 ($feat:literal) => {
261
262 #[test_log::test]
263 #[cfg_attr(not(feature = $feat), ignore = "API test")]
264 fn create_update_v4() -> Result<()> {
265 test_create_update_delete_ipv4(get_client())?;
266 Ok(())
267 }
268
269 #[test_log::test]
270 #[cfg_attr(not(feature = $feat), ignore = "API test")]
271 fn create_update_txt() -> Result<()> {
272 test_create_update_delete_txt(get_client())?;
273 Ok(())
274 }
275
276 #[test_log::test]
277 #[cfg_attr(not(feature = $feat), ignore = "API test")]
278 fn create_update_default() -> Result<()> {
279 test_create_update_delete_txt_default(get_client())?;
280 Ok(())
281 }
282 }
283 }
284
285
286}