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