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