1mod types;
2
3use std::fmt::Display;
4
5use serde::{de::DeserializeOwned, Deserialize, Serialize};
6use tracing::{error, info, warn};
7
8use crate::{
9 Config, DnsProvider, RecordType,
10 desec::types::{CreateUpdateRRSet, RRSet},
11 errors::{Error, Result},
12 generate_helpers,
13 http::{self, ResponseToOption, WithHeaders},
14};
15
16const API_BASE: &'static str = "https://desec.io/api/v1";
17
18#[derive(Clone, Debug, Deserialize)]
22pub struct Auth {
23 pub key: String,
24}
25
26impl Auth {
27 fn get_header(&self) -> String {
28 format!("Token {}", self.key)
29 }
30}
31
32pub struct DeSec {
36 config: Config,
37 auth: Auth,
38}
39
40impl DeSec {
41 pub fn new(config: Config, auth: Auth) -> Self {
43 Self {
44 config,
45 auth,
46 }
47 }
48
49}
50
51impl DnsProvider for DeSec {
52
53 fn get_record<T>(&self, rtype: RecordType, host: &str) -> Result<Option<T>>
54 where
55 T: DeserializeOwned
56 {
57
58 let url = format!("{API_BASE}/domains/{}/rrsets/{host}/{rtype}/", self.config.domain);
59 let response = http::client().get(url)
60 .with_json_headers()
61 .with_auth(self.auth.get_header())
62 .call()?
63 .to_option::<RRSet<T>>()?;
64
65 let mut rec: RRSet<T> = match response {
66 Some(rec) => rec,
67 None => return Ok(None)
68 };
69
70 let nr = rec.records.len();
71
72 if nr > 1 {
75 error!("Returned number of IPs is {}, should be 1", nr);
76 return Err(Error::UnexpectedRecord(format!("Returned number of IPs is {nr}, should be 1")));
77 } else if nr == 0 {
78 warn!("No IP returned for {host}, continuing");
79 return Ok(None);
80 }
81
82 Ok(Some(rec.records.remove(0)))
83
84 }
85
86 fn create_record<T>(&self, rtype: RecordType, host: &str, record: &T) -> Result<()>
87 where
88 T: Serialize + DeserializeOwned + Display + Clone
89 {
90 let url = format!("{API_BASE}/domains/{}/rrsets/", self.config.domain);
91
92 let record = CreateUpdateRRSet {
93 subname: host.to_string(),
94 rtype,
95 records: vec![record.to_string()],
96 ttl: 3600, };
98 if self.config.dry_run {
99 info!("DRY-RUN: Would have sent {record:?} to {url}");
100 return Ok(())
101 }
102
103 let body = serde_json::to_string(&record)?;
104 let _response = http::client().post(url)
105 .with_json_headers()
106 .with_auth(self.auth.get_header())
107 .send(body)?
108 .check_error()?;
109
110 Ok(())
111 }
112
113 fn update_record<T>(&self, rtype: RecordType, host: &str, urec: &T) -> Result<()>
114 where
115 T: Serialize + DeserializeOwned + Display + Clone
116 {
117 let url = format!("{API_BASE}/domains/{}/rrsets/{host}/{rtype}/", self.config.domain);
118
119 let record = CreateUpdateRRSet {
120 subname: host.to_string(),
121 rtype,
122 records: vec![urec.to_string()],
123 ttl: 3600, };
125
126 if self.config.dry_run {
127 info!("DRY-RUN: Would have sent {record:?} to {url}");
128 return Ok(())
129 }
130
131 let body = serde_json::to_string(&record)?;
132 let _response = http::client().put(url)
133 .with_json_headers()
134 .with_auth(self.auth.get_header())
135 .send(body)?
136 .check_error()?;
137
138 Ok(())
139 }
140
141 fn delete_record(&self, rtype: RecordType, host: &str) -> Result<()>
142 {
143 let url = format!("{API_BASE}/domains/{}/rrsets/{host}/{rtype}/", self.config.domain);
144 if self.config.dry_run {
145 info!("DRY-RUN: Would have sent DELETE to {url}");
146 return Ok(())
147 }
148
149 http::client().delete(url)
150 .with_json_headers()
151 .with_auth(self.auth.get_header())
152 .call()?;
153
154 Ok(())
155 }
156
157 generate_helpers!();
158
159}
160
161
162#[cfg(test)]
163pub(crate) mod tests {
164 use super::*;
165 use crate::{generate_tests, tests::*};
166 use std::env;
167
168 fn get_client() -> DeSec{
169 let auth = Auth {
170 key: env::var("DESEC_API_KEY").unwrap(),
171 };
172 let config = Config {
173 domain: env::var("DESEC_TEST_DOMAIN").unwrap(),
174 dry_run: false,
175 };
176 DeSec::new(config, auth)
177 }
178
179 generate_tests!("test_desec");
180}