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