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}