1use serde_json::Value;
2
3use crate::control_plane::config::{self, DnsServerConfig, VendorKind};
4use crate::core::dns::capabilities::VendorCapabilities;
5use crate::core::dns::logs::{LogLine, LogsOptions, LogsRead};
6use crate::core::dns::records::RecordData;
7use crate::core::dns::responses::ListRecordsResponse;
8use crate::core::dns::service::{
9 AccessListRead, AccessListWrite, CacheRead, CacheWrite, DnsVendor, ListRecordsOptions,
10 RecordWrite, SettingsRead, StatsRead, ZoneExport, ZoneImport, ZoneRead, ZoneWrite,
11};
12use crate::core::error::{Error, Result};
13
14#[derive(Debug, Clone, Copy, Default)]
15pub struct ClientOverrides<'a> {
16 pub selected_server: Option<&'a str>,
17 pub base_url: Option<&'a str>,
18 pub token: Option<&'a str>,
19}
20
21#[derive(Clone, Debug)]
22pub enum VendorClient {
23 #[cfg(feature = "technitium")]
24 Technitium(crate::vendors::technitium::client::TechnitiumClient),
25 #[cfg(feature = "pangolin")]
26 Pangolin(crate::vendors::pangolin::client::PangolinClient),
27 #[cfg(feature = "cloudflare")]
28 Cloudflare(crate::vendors::cloudflare::client::CloudflareClient),
29 #[cfg(feature = "unifi")]
30 Unifi(crate::vendors::unifi::client::UnifiClient),
31 #[cfg(feature = "pihole")]
32 Pihole(crate::vendors::pihole::client::PiholeClient),
33}
34
35impl VendorClient {
36 pub fn from_cli_options(
37 app_config: Option<&config::AppConfig>,
38 overrides: ClientOverrides<'_>,
39 ) -> Result<Self> {
40 let Some(app_config) = app_config else {
41 return Self::client_without_config(overrides);
42 };
43
44 let server = app_config.selected_server(overrides.selected_server)?;
45 Self::from_selected_server(server, overrides)
46 }
47
48 pub fn from_server(server: &DnsServerConfig) -> Result<Self> {
49 match server.vendor {
50 #[cfg(feature = "technitium")]
51 VendorKind::Technitium => Ok(Self::Technitium(
52 crate::vendors::technitium::client_from_server(server, ClientOverrides::default())?,
53 )),
54 #[cfg(feature = "pangolin")]
55 VendorKind::Pangolin => Ok(Self::Pangolin(
56 crate::vendors::pangolin::client_from_server(server, ClientOverrides::default())?,
57 )),
58 #[cfg(feature = "cloudflare")]
59 VendorKind::Cloudflare => Ok(Self::Cloudflare(
60 crate::vendors::cloudflare::client_from_server(server, ClientOverrides::default())?,
61 )),
62 #[cfg(feature = "unifi")]
63 VendorKind::Unifi => Ok(Self::Unifi(crate::vendors::unifi::client_from_server(
64 server,
65 ClientOverrides::default(),
66 )?)),
67 #[cfg(feature = "pihole")]
68 VendorKind::Pihole => Ok(Self::Pihole(crate::vendors::pihole::client_from_server(
69 server,
70 ClientOverrides::default(),
71 )?)),
72 #[allow(unreachable_patterns)]
73 _ => Err(Error::parse(format!(
74 "server '{}' has unsupported vendor in this build",
75 server.id
76 ))),
77 }
78 }
79
80 pub async fn export_zone_for_server(server: &DnsServerConfig, zone: &str) -> Result<String> {
81 let _ = zone;
82 match server.vendor {
85 #[cfg(feature = "technitium")]
86 VendorKind::Technitium => {
87 let client = crate::vendors::technitium::client_from_server(
88 server,
89 ClientOverrides::default(),
90 )?;
91 client.export_zone_file(zone).await
92 }
93 #[cfg(feature = "cloudflare")]
94 VendorKind::Cloudflare => {
95 let client = crate::vendors::cloudflare::client_from_server(
96 server,
97 ClientOverrides::default(),
98 )?;
99 client.export_zone_file(zone).await
100 }
101 #[cfg(feature = "pangolin")]
102 VendorKind::Pangolin => Err(Error::unsupported("Pangolin", "zone export")),
103 #[cfg(feature = "unifi")]
104 VendorKind::Unifi => Err(Error::unsupported("UniFi", "zone export")),
105 #[cfg(feature = "pihole")]
106 VendorKind::Pihole => Err(Error::unsupported("Pi-hole", "zone export")),
107 #[allow(unreachable_patterns)]
108 _ => Err(Error::parse(format!(
109 "server '{}' has unsupported vendor in this build",
110 server.id
111 ))),
112 }
113 }
114
115 pub async fn import_zone_for_server(
116 server: &DnsServerConfig,
117 zone: &str,
118 file_name: String,
119 file_bytes: Vec<u8>,
120 overwrite: bool,
121 overwrite_zone: bool,
122 ) -> Result<Value> {
123 let _ = (zone, &file_name, &file_bytes, overwrite, overwrite_zone);
124 match server.vendor {
127 #[cfg(feature = "technitium")]
128 VendorKind::Technitium => {
129 let client = crate::vendors::technitium::client_from_server(
130 server,
131 ClientOverrides::default(),
132 )?;
133 client
134 .import_zone_file(
135 zone,
136 file_name,
137 file_bytes,
138 overwrite,
139 overwrite_zone,
140 false,
141 )
142 .await
143 }
144 #[cfg(feature = "cloudflare")]
145 VendorKind::Cloudflare => {
146 let client = crate::vendors::cloudflare::client_from_server(
147 server,
148 ClientOverrides::default(),
149 )?;
150 client
151 .import_zone_file(
152 zone,
153 file_name,
154 file_bytes,
155 overwrite,
156 overwrite_zone,
157 false,
158 )
159 .await
160 }
161 #[cfg(feature = "pangolin")]
162 VendorKind::Pangolin => Err(Error::unsupported("Pangolin", "zone import")),
163 #[cfg(feature = "unifi")]
164 VendorKind::Unifi => Err(Error::unsupported("UniFi", "zone import")),
165 #[cfg(feature = "pihole")]
166 VendorKind::Pihole => Err(Error::unsupported("Pi-hole", "zone import")),
167 #[allow(unreachable_patterns)]
168 _ => Err(Error::parse(format!(
169 "server '{}' has unsupported vendor in this build",
170 server.id
171 ))),
172 }
173 }
174
175 fn from_selected_server(
176 server: &DnsServerConfig,
177 overrides: ClientOverrides<'_>,
178 ) -> Result<Self> {
179 match server.vendor {
180 #[cfg(feature = "technitium")]
181 VendorKind::Technitium => Ok(Self::Technitium(
182 crate::vendors::technitium::client_from_server(server, overrides)?,
183 )),
184 #[cfg(feature = "pangolin")]
185 VendorKind::Pangolin => Ok(Self::Pangolin(
186 crate::vendors::pangolin::client_from_server(server, overrides)?,
187 )),
188 #[cfg(feature = "cloudflare")]
189 VendorKind::Cloudflare => Ok(Self::Cloudflare(
190 crate::vendors::cloudflare::client_from_server(server, overrides)?,
191 )),
192 #[cfg(feature = "unifi")]
193 VendorKind::Unifi => Ok(Self::Unifi(crate::vendors::unifi::client_from_server(
194 server, overrides,
195 )?)),
196 #[cfg(feature = "pihole")]
197 VendorKind::Pihole => Ok(Self::Pihole(crate::vendors::pihole::client_from_server(
198 server, overrides,
199 )?)),
200 #[allow(unreachable_patterns)]
201 _ => Err(Error::parse(format!(
202 "server '{}' has unsupported vendor in this build",
203 server.id
204 ))),
205 }
206 }
207
208 #[cfg(feature = "technitium")]
209 fn client_without_config(overrides: ClientOverrides<'_>) -> Result<Self> {
210 Ok(Self::Technitium(
211 crate::vendors::technitium::client_from_cli_without_config(overrides)?,
212 ))
213 }
214
215 #[cfg(not(feature = "technitium"))]
216 fn client_without_config(_overrides: ClientOverrides<'_>) -> Result<Self> {
217 Err(Error::parse(
218 "Technitium vendor is not supported in this build",
219 ))
220 }
221}
222
223macro_rules! delegate_vendor {
224 ($self:expr, $client:ident => $body:expr) => {
225 match $self {
226 #[cfg(feature = "technitium")]
227 Self::Technitium($client) => $body,
228 #[cfg(feature = "pangolin")]
229 Self::Pangolin($client) => $body,
230 #[cfg(feature = "cloudflare")]
231 Self::Cloudflare($client) => $body,
232 #[cfg(feature = "unifi")]
233 Self::Unifi($client) => $body,
234 #[cfg(feature = "pihole")]
235 Self::Pihole($client) => $body,
236 }
237 };
238}
239
240impl DnsVendor for VendorClient {
241 fn kind(&self) -> VendorKind {
242 delegate_vendor!(self, client => client.kind())
243 }
244
245 fn capabilities(&self) -> VendorCapabilities {
246 delegate_vendor!(self, client => client.capabilities())
247 }
248}
249
250impl ZoneRead for VendorClient {
251 async fn list_zones(&self, page: u32, per_page: u32) -> Result<Value> {
252 delegate_vendor!(self, client => client.list_zones(page, per_page).await)
253 }
254
255 async fn list_records(
256 &self,
257 domain: &str,
258 zone: Option<&str>,
259 options: ListRecordsOptions,
260 ) -> Result<ListRecordsResponse> {
261 delegate_vendor!(self, client => client.list_records(domain, zone, options).await)
262 }
263}
264
265impl ZoneWrite for VendorClient {
266 async fn create_zone(&self, zone: &str, zone_type: &str) -> Result<Value> {
267 delegate_vendor!(self, client => client.create_zone(zone, zone_type).await)
268 }
269
270 async fn delete_zone(&self, zone: &str) -> Result<Value> {
271 delegate_vendor!(self, client => client.delete_zone(zone).await)
272 }
273
274 async fn enable_zone(&self, zone: &str) -> Result<Value> {
275 delegate_vendor!(self, client => client.enable_zone(zone).await)
276 }
277
278 async fn disable_zone(&self, zone: &str) -> Result<Value> {
279 delegate_vendor!(self, client => client.disable_zone(zone).await)
280 }
281}
282
283impl RecordWrite for VendorClient {
284 async fn add_record(
285 &self,
286 zone: &str,
287 domain: &str,
288 ttl: u32,
289 record: &RecordData,
290 ) -> Result<Value> {
291 delegate_vendor!(self, client => client.add_record(zone, domain, ttl, record).await)
292 }
293
294 async fn delete_record(
295 &self,
296 zone: &str,
297 domain: &str,
298 type_params: &[(&str, String)],
299 ) -> Result<Value> {
300 delegate_vendor!(self, client => client.delete_record(zone, domain, type_params).await)
301 }
302}
303
304impl CacheRead for VendorClient {
305 async fn list_cache(&self, domain: &str) -> Result<Value> {
306 delegate_vendor!(self, client => client.list_cache(domain).await)
307 }
308}
309
310impl CacheWrite for VendorClient {
311 async fn delete_cache_zone(&self, domain: &str) -> Result<Value> {
312 delegate_vendor!(self, client => client.delete_cache_zone(domain).await)
313 }
314
315 async fn flush_cache(&self) -> Result<Value> {
316 delegate_vendor!(self, client => client.flush_cache().await)
317 }
318}
319
320impl StatsRead for VendorClient {
321 async fn get_stats(&self, stats_type: &str) -> Result<Value> {
322 delegate_vendor!(self, client => client.get_stats(stats_type).await)
323 }
324}
325
326impl AccessListRead for VendorClient {
327 async fn list_blocked(&self) -> Result<Value> {
328 delegate_vendor!(self, client => client.list_blocked().await)
329 }
330
331 async fn list_allowed(&self) -> Result<Value> {
332 delegate_vendor!(self, client => client.list_allowed().await)
333 }
334}
335
336impl AccessListWrite for VendorClient {
337 async fn add_blocked(&self, domain: &str) -> Result<Value> {
338 delegate_vendor!(self, client => client.add_blocked(domain).await)
339 }
340
341 async fn delete_blocked(&self, domain: &str) -> Result<Value> {
342 delegate_vendor!(self, client => client.delete_blocked(domain).await)
343 }
344
345 async fn add_allowed(&self, domain: &str) -> Result<Value> {
346 delegate_vendor!(self, client => client.add_allowed(domain).await)
347 }
348
349 async fn delete_allowed(&self, domain: &str) -> Result<Value> {
350 delegate_vendor!(self, client => client.delete_allowed(domain).await)
351 }
352}
353
354impl ZoneImport for VendorClient {
355 async fn import_zone_file(
356 &self,
357 zone: &str,
358 file_name: String,
359 file_bytes: Vec<u8>,
360 overwrite: bool,
361 overwrite_zone: bool,
362 overwrite_soa_serial: bool,
363 ) -> Result<Value> {
364 delegate_vendor!(self, client => {
365 client
366 .import_zone_file(
367 zone,
368 file_name,
369 file_bytes,
370 overwrite,
371 overwrite_zone,
372 overwrite_soa_serial,
373 )
374 .await
375 })
376 }
377}
378
379impl ZoneExport for VendorClient {
380 async fn export_zone_file(&self, zone: &str) -> Result<String> {
381 delegate_vendor!(self, client => client.export_zone_file(zone).await)
382 }
383}
384
385impl SettingsRead for VendorClient {
386 async fn get_settings(&self) -> Result<Value> {
387 delegate_vendor!(self, client => client.get_settings().await)
388 }
389}
390
391impl LogsRead for VendorClient {
392 async fn get_logs(&self, options: LogsOptions) -> Result<Vec<LogLine>> {
393 delegate_vendor!(self, client => client.get_logs(options).await)
394 }
395}
396
397#[cfg(test)]
398mod tests {
399 use super::*;
400
401 #[cfg(feature = "technitium")]
402 #[test]
403 fn default_without_config_builds_technitium_client() {
404 let client = VendorClient::from_cli_options(
405 None,
406 ClientOverrides {
407 token: Some("token"),
408 ..ClientOverrides::default()
409 },
410 )
411 .unwrap();
412
413 assert_eq!(client.kind(), VendorKind::Technitium);
414 }
415}