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
16use std::{fmt::{self, Debug, Display, Formatter}, net::Ipv4Addr};
17
18use serde::{de::DeserializeOwned, Deserialize, Serialize};
19use tracing::warn;
20
21use crate::errors::Result;
22
23
24pub struct Config {
25 pub domain: String,
26 pub dry_run: bool,
27}
28
29#[derive(Serialize, Deserialize, Clone, Debug)]
30pub enum RecordType {
31 A,
32 AAAA,
33 CAA,
34 CNAME,
35 HINFO,
36 MX,
37 NAPTR,
38 NS,
39 PTR,
40 SRV,
41 SPF,
42 SSHFP,
43 TXT,
44}
45
46impl Display for RecordType {
47 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
48 write!(f, "{:?}", self)
49 }
50}
51
52#[allow(unused)]
53pub trait DnsProvider {
54 fn get_record<T>(&self, rtype: RecordType, host: &str) -> Result<Option<T>>
55 where
56 T: DeserializeOwned;
57
58 fn create_record<T>(&self, rtype: RecordType, host: &str, record: &T) -> Result<()>
59 where
60 T: Serialize + DeserializeOwned + Display + Clone;
61
62 fn update_record<T>(&self, rtype: RecordType, host: &str, record: &T) -> Result<()>
63 where
64 T: Serialize + DeserializeOwned + Display + Clone;
65
66 fn delete_record(&self, rtype: RecordType, host: &str) -> Result<()>;
67
68
69 fn get_txt_record(&self, host: &str) -> Result<Option<String>> {
72 self.get_record::<String>(RecordType::TXT, host)
73 .map(|opt| opt.map(|s| strip_quotes(&s)))
74 }
75
76 fn create_txt_record(&self, host: &str, record: &String) -> Result<()> {
77 self.create_record(RecordType::TXT, host, record)
78 }
79
80 fn update_txt_record(&self, host: &str, record: &String) -> Result<()> {
81 self.update_record(RecordType::TXT, host, record)
82 }
83
84 fn delete_txt_record(&self, host: &str) -> Result<()> {
85 self.delete_record(RecordType::TXT, host)
86 }
87
88 fn get_a_record(&self, host: &str) -> Result<Option<Ipv4Addr>> {
89 self.get_record(RecordType::A, host)
90 }
91
92 fn create_a_record(&self, host: &str, record: &Ipv4Addr) -> Result<()> {
93 self.create_record(RecordType::A, host, record)
94 }
95
96 fn update_a_record(&self, host: &str, record: &Ipv4Addr) -> Result<()> {
97 self.update_record(RecordType::A, host, record)
98 }
99
100 fn delete_a_record(&self, host: &str) -> Result<()> {
101 self.delete_record(RecordType::A, host)
102 }
103}
104
105
106fn strip_quotes(record: &str) -> String {
107 let chars = record.chars();
108 let mut check = chars.clone();
109
110 let first = check.next();
111 let last = check.last();
112
113 if let Some('"') = first && let Some('"') = last {
114 chars.skip(1)
115 .take(record.len() - 2)
116 .collect()
117
118 } else {
119 warn!("Double quotes not found in record string, using whole record.");
120 record.to_string()
121 }
122}
123
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128 use std::net::Ipv4Addr;
129 use random_string::charsets::ALPHA_LOWER;
130 use tracing::info;
131
132 #[test]
133 fn test_strip_quotes() -> Result<()> {
134 assert_eq!("abc123".to_string(), strip_quotes("\"abc123\""));
135 assert_eq!("abc123\"", strip_quotes("abc123\""));
136 assert_eq!("\"abc123", strip_quotes("\"abc123"));
137 assert_eq!("abc123", strip_quotes("abc123"));
138
139 Ok(())
140 }
141
142
143 pub(crate) fn test_create_update_delete_ipv4(client: impl DnsProvider) -> Result<()> {
144
145 let host = random_string::generate(16, ALPHA_LOWER);
146
147 info!("Creating IPv4 {host}");
149 let ip: Ipv4Addr = "1.1.1.1".parse()?;
150 client.create_record(RecordType::A, &host, &ip)?;
151 let cur = client.get_record(RecordType::A, &host)?;
152 assert_eq!(Some(ip), cur);
153
154
155 info!("Updating IPv4 {host}");
157 let ip: Ipv4Addr = "2.2.2.2".parse()?;
158 client.update_record(RecordType::A, &host, &ip)?;
159 let cur = client.get_record(RecordType::A, &host)?;
160 assert_eq!(Some(ip), cur);
161
162
163 info!("Deleting IPv4 {host}");
165 client.delete_record(RecordType::A, &host)?;
166 let del: Option<Ipv4Addr> = client.get_record(RecordType::A, &host)?;
167 assert!(del.is_none());
168
169 Ok(())
170 }
171
172 pub(crate) fn test_create_update_delete_txt(client: impl DnsProvider) -> Result<()> {
173
174 let host = random_string::generate(16, ALPHA_LOWER);
175
176 let txt = "a text reference".to_string();
178 client.create_record(RecordType::TXT, &host, &txt)?;
179 let cur: Option<String> = client.get_record(RecordType::TXT, &host)?;
180 assert_eq!(txt, strip_quotes(&cur.unwrap()));
181
182
183 let txt = "another text reference".to_string();
185 client.update_record(RecordType::TXT, &host, &txt)?;
186 let cur: Option<String> = client.get_record(RecordType::TXT, &host)?;
187 assert_eq!(txt, strip_quotes(&cur.unwrap()));
188
189
190 client.delete_record(RecordType::TXT, &host)?;
192 let del: Option<String> = client.get_record(RecordType::TXT, &host)?;
193 assert!(del.is_none());
194
195 Ok(())
196 }
197
198 pub(crate) fn test_create_update_delete_txt_default(client: impl DnsProvider) -> Result<()> {
199
200 let host = random_string::generate(16, ALPHA_LOWER);
201
202 let txt = "a text reference".to_string();
204 client.create_txt_record(&host, &txt)?;
205 let cur = client.get_txt_record(&host)?;
206 assert_eq!(txt, strip_quotes(&cur.unwrap()));
207
208
209 let txt = "another text reference".to_string();
211 client.update_txt_record(&host, &txt)?;
212 let cur = client.get_txt_record(&host)?;
213 assert_eq!(txt, strip_quotes(&cur.unwrap()));
214
215
216 client.delete_txt_record(&host)?;
218 let del = client.get_txt_record(&host)?;
219 assert!(del.is_none());
220
221 Ok(())
222 }
223
224 #[macro_export]
257 macro_rules! generate_tests {
258 ($feat:literal) => {
259
260 #[test_log::test]
261 #[cfg_attr(not(feature = $feat), ignore = "API test")]
262 fn create_update_v4() -> Result<()> {
263 test_create_update_delete_ipv4(get_client())?;
264 Ok(())
265 }
266
267 #[test_log::test]
268 #[cfg_attr(not(feature = $feat), ignore = "API test")]
269 fn create_update_txt() -> Result<()> {
270 test_create_update_delete_txt(get_client())?;
271 Ok(())
272 }
273
274 #[test_log::test]
275 #[cfg_attr(not(feature = $feat), ignore = "API test")]
276 fn create_update_default() -> Result<()> {
277 test_create_update_delete_txt_default(get_client())?;
278 Ok(())
279 }
280 }
281 }
282
283
284}