1use log::{debug, info};
6use reqwest::Client as Reqw;
7use std::net::IpAddr;
8
9pub struct Dynu {
10 token: String,
11 client: Reqw,
12}
13
14use serde::{Deserialize, Serialize};
15
16#[derive(Deserialize, Debug)]
19#[serde(rename_all = "camelCase")]
20pub struct DynuRecord {
21 id: u32,
22 domain_id: u32,
23 ipv4_address: IpAddr,
27}
28
29#[derive(Deserialize, Debug)]
30#[serde(rename_all = "camelCase")]
31struct DynuRecordsResp {
32 dns_records: Vec<DynuRecord>,
34}
35
36#[derive(Deserialize, Debug)]
37struct DynuGetRoot {
38 id: u32,
40 }
44
45#[derive(Serialize, Debug)]
46#[serde(rename_all = "camelCase")]
47struct DynuAddPayload {
48 group: String,
49 ipv4_address: IpAddr,
50 node_name: String,
51 record_type: String,
52 state: bool,
53 ttl: u32,
54}
55
56fn extract_record_name(fqdn: &str, zone: &str) -> String {
57 fqdn[..(fqdn.len() - zone.len() - 1)].to_owned()
58}
59
60impl Dynu {
61 pub fn new(token: String) -> Self {
62 let client = Reqw::builder()
63 .user_agent("rrdnsd")
64 .pool_max_idle_per_host(0)
65 .build()
66 .unwrap();
67 Dynu { token, client }
68 }
69
70 pub async fn add_record(&self, fqdn: &str, zone: &str, ipaddr: IpAddr) {
71 debug!("[dynu] Fetching domain ID for {fqdn}");
72 let domain_id = {
73 let url = format!("https://api.dynu.com/v2/dns/getroot/{fqdn}");
74 let res = self
75 .client
76 .get(url)
77 .header("API-Key", self.token.clone())
78 .send()
79 .await
80 .unwrap();
81 let r = res.json::<DynuGetRoot>().await.unwrap();
82 r.id
83 };
84 debug!("[dynu] Received domain ID {domain_id}");
85
86 let url = format!("https://api.dynu.com/v2/dns/{domain_id}/record");
87 let payload = DynuAddPayload {
88 group: String::new(),
89 ipv4_address: ipaddr,
90 node_name: extract_record_name(fqdn, zone),
91 record_type: "A".to_owned(),
92 state: true,
93 ttl: 30,
94 };
95 debug!("[dynu] Adding record domain ID {domain_id}");
96 let res = self
97 .client
98 .post(url)
99 .header("API-Key", self.token.clone())
100 .json(&payload)
101 .send()
102 .await
103 .unwrap();
104 let qq = res.text().await.unwrap();
105 let v: serde_json::Value = serde_json::from_str(&qq).unwrap();
106 info!("{v:?}");
107 }
108
109 pub async fn delete_record(&self, fqdn: &str, _zone: &str, ipaddr: IpAddr) {
110 let existing_record: DynuRecord = 'found: {
111 let url = format!("https://api.dynu.com/v2/dns/record/{fqdn}?recordType=A");
112 let res = self
113 .client
114 .get(url)
115 .header("API-Key", self.token.clone())
116 .send()
117 .await
118 .unwrap();
119 let j = res.text().await.unwrap();
120 let r: DynuRecordsResp = serde_json::from_str(&j).unwrap();
121 for rdata in r.dns_records {
122 if rdata.ipv4_address == ipaddr {
123 break 'found rdata;
124 }
125 }
126 info!("not found");
127 return;
128 };
129 debug!("Found record {existing_record:?}");
130 let url = format!(
131 "https://api.dynu.com/v2/dns/{}/record/{}",
132 existing_record.domain_id, existing_record.id
133 );
134 let res = self
135 .client
136 .delete(url.clone())
137 .header("API-Key", self.token.clone())
138 .header("accept", "application/json")
139 .header("Content-Type", "application/json")
140 .send()
141 .await
142 .unwrap();
143 let qq = res.text().await.unwrap();
145 let v: serde_json::Value = serde_json::from_str(&qq).unwrap();
146 info!("{v:?}");
147 }
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153 #[test]
154 fn test_extract_record_name() {
155 assert_eq!(extract_record_name("a.b.c.d", "b.c.d"), "a");
156 assert_eq!(extract_record_name("aaa.b.c.d", "b.c.d"), "aaa");
157 }
158}