Skip to main content

dnslib/vendors/
runtime.rs

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        // Keep unsupported vendors from resolving credentials before reporting
82        // capability errors; zone transfer should fail on support, not auth.
83        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        // Keep unsupported vendors from resolving credentials before reporting
124        // capability errors; zone transfer should fail on support, not auth.
125        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}