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
65 T: DeserializeOwned;
66
67 fn create_record<T>(&self, rtype: RecordType, host: &str, record: &T) -> Result<()>
69 where
70 T: Serialize + DeserializeOwned + Display + Clone;
71
72 fn update_record<T>(&self, rtype: RecordType, host: &str, record: &T) -> Result<()>
74 where
75 T: Serialize + DeserializeOwned + Display + Clone;
76
77 fn delete_record(&self, rtype: RecordType, host: &str) -> Result<()>;
79
80
81 fn get_txt_record(&self, host: &str) -> Result<Option<String>> {
85 self.get_record::<String>(RecordType::TXT, host)
86 .map(|opt| opt.map(|s| strip_quotes(&s)))
87 }
88
89 fn create_txt_record(&self, host: &str, record: &String) -> Result<()> {
93 self.create_record(RecordType::TXT, host, record)
94 }
95
96 fn update_txt_record(&self, host: &str, record: &String) -> Result<()> {
100 self.update_record(RecordType::TXT, host, record)
101 }
102
103 fn delete_txt_record(&self, host: &str) -> Result<()> {
107 self.delete_record(RecordType::TXT, host)
108 }
109
110 fn get_a_record(&self, host: &str) -> Result<Option<Ipv4Addr>> {
114 self.get_record(RecordType::A, host)
115 }
116
117 fn create_a_record(&self, host: &str, record: &Ipv4Addr) -> Result<()> {
121 self.create_record(RecordType::A, host, record)
122 }
123
124 fn update_a_record(&self, host: &str, record: &Ipv4Addr) -> Result<()> {
128 self.update_record(RecordType::A, host, record)
129 }
130
131 fn delete_a_record(&self, host: &str) -> Result<()> {
135 self.delete_record(RecordType::A, host)
136 }
137}
138
139
140fn strip_quotes(record: &str) -> String {
141 let chars = record.chars();
142 let mut check = chars.clone();
143
144 let first = check.next();
145 let last = check.last();
146
147 if let Some('"') = first && let Some('"') = last {
148 chars.skip(1)
149 .take(record.len() - 2)
150 .collect()
151
152 } else {
153 warn!("Double quotes not found in record string, using whole record.");
154 record.to_string()
155 }
156}
157
158
159#[cfg(test)]
160mod tests {
161 use super::*;
162 use std::net::Ipv4Addr;
163 use random_string::charsets::ALPHA_LOWER;
164 use tracing::info;
165
166 #[test]
167 fn test_strip_quotes() -> Result<()> {
168 assert_eq!("abc123".to_string(), strip_quotes("\"abc123\""));
169 assert_eq!("abc123\"", strip_quotes("abc123\""));
170 assert_eq!("\"abc123", strip_quotes("\"abc123"));
171 assert_eq!("abc123", strip_quotes("abc123"));
172
173 Ok(())
174 }
175
176
177 pub(crate) fn test_create_update_delete_ipv4(client: impl DnsProvider) -> Result<()> {
178
179 let host = random_string::generate(16, ALPHA_LOWER);
180
181 info!("Creating IPv4 {host}");
183 let ip: Ipv4Addr = "1.1.1.1".parse()?;
184 client.create_record(RecordType::A, &host, &ip)?;
185 let cur = client.get_record(RecordType::A, &host)?;
186 assert_eq!(Some(ip), cur);
187
188
189 info!("Updating IPv4 {host}");
191 let ip: Ipv4Addr = "2.2.2.2".parse()?;
192 client.update_record(RecordType::A, &host, &ip)?;
193 let cur = client.get_record(RecordType::A, &host)?;
194 assert_eq!(Some(ip), cur);
195
196
197 info!("Deleting IPv4 {host}");
199 client.delete_record(RecordType::A, &host)?;
200 let del: Option<Ipv4Addr> = client.get_record(RecordType::A, &host)?;
201 assert!(del.is_none());
202
203 Ok(())
204 }
205
206 pub(crate) fn test_create_update_delete_txt(client: impl DnsProvider) -> Result<()> {
207
208 let host = random_string::generate(16, ALPHA_LOWER);
209
210 let txt = "a text reference".to_string();
212 client.create_record(RecordType::TXT, &host, &txt)?;
213 let cur: Option<String> = client.get_record(RecordType::TXT, &host)?;
214 assert_eq!(txt, strip_quotes(&cur.unwrap()));
215
216
217 let txt = "another text reference".to_string();
219 client.update_record(RecordType::TXT, &host, &txt)?;
220 let cur: Option<String> = client.get_record(RecordType::TXT, &host)?;
221 assert_eq!(txt, strip_quotes(&cur.unwrap()));
222
223
224 client.delete_record(RecordType::TXT, &host)?;
226 let del: Option<String> = client.get_record(RecordType::TXT, &host)?;
227 assert!(del.is_none());
228
229 Ok(())
230 }
231
232 pub(crate) fn test_create_update_delete_txt_default(client: impl DnsProvider) -> Result<()> {
233
234 let host = random_string::generate(16, ALPHA_LOWER);
235
236 let txt = "a text reference".to_string();
238 client.create_txt_record(&host, &txt)?;
239 let cur = client.get_txt_record(&host)?;
240 assert_eq!(txt, strip_quotes(&cur.unwrap()));
241
242
243 let txt = "another text reference".to_string();
245 client.update_txt_record(&host, &txt)?;
246 let cur = client.get_txt_record(&host)?;
247 assert_eq!(txt, strip_quotes(&cur.unwrap()));
248
249
250 client.delete_txt_record(&host)?;
252 let del = client.get_txt_record(&host)?;
253 assert!(del.is_none());
254
255 Ok(())
256 }
257
258 #[macro_export]
291 macro_rules! generate_tests {
292 ($feat:literal) => {
293
294 #[test_log::test]
295 #[cfg_attr(not(feature = $feat), ignore = "API test")]
296 fn create_update_v4() -> Result<()> {
297 test_create_update_delete_ipv4(get_client())?;
298 Ok(())
299 }
300
301 #[test_log::test]
302 #[cfg_attr(not(feature = $feat), ignore = "API test")]
303 fn create_update_txt() -> Result<()> {
304 test_create_update_delete_txt(get_client())?;
305 Ok(())
306 }
307
308 #[test_log::test]
309 #[cfg_attr(not(feature = $feat), ignore = "API test")]
310 fn create_update_default() -> Result<()> {
311 test_create_update_delete_txt_default(get_client())?;
312 Ok(())
313 }
314 }
315 }
316
317
318}