pi_hole_api/
lib.rs

1use crate::fake_hash_map::FakeHashMap;
2use serde::de::DeserializeOwned;
3use std::collections::HashMap;
4use std::net::IpAddr;
5pub mod api_types;
6mod custom_deserializers;
7pub mod errors;
8mod fake_hash_map;
9pub mod ftl_types;
10use crate::api_types::*;
11use std::borrow::Borrow;
12
13const NO_PARAMS: [(&str, &str); 0] = [];
14
15trait PiHoleAPIHost {
16    fn get_host(&self) -> &str;
17}
18
19trait PiHoleAPIKey {
20    fn get_api_key(&self) -> &str;
21}
22
23/// Pi Hole API Struct
24#[derive(Debug)]
25pub struct PiHoleAPIConfig {
26    /// Pi Hole host
27    host: String,
28}
29
30impl PiHoleAPIConfig {
31    /// Creates a new Pi Hole API instance.
32    /// `host` must begin with the protocol e.g. http:// or https://
33    pub fn new(host: String) -> Self {
34        Self { host }
35    }
36}
37
38/// Pi Hole API Struct
39#[derive(Debug)]
40pub struct PiHoleAPIConfigWithKey {
41    /// Pi Hole host
42    host: String,
43
44    /// API key
45    api_key: String,
46}
47
48impl PiHoleAPIConfigWithKey {
49    /// Creates a new Pi Hole API instance.
50    /// `host` must begin with the protocol e.g. http:// or https://
51    pub fn new(host: String, api_key: String) -> Self {
52        Self { host, api_key }
53    }
54}
55
56impl PiHoleAPIHost for PiHoleAPIConfig {
57    fn get_host(&self) -> &str {
58        &self.host
59    }
60}
61
62impl PiHoleAPIHost for PiHoleAPIConfigWithKey {
63    fn get_host(&self) -> &str {
64        &self.host
65    }
66}
67
68impl PiHoleAPIKey for PiHoleAPIConfigWithKey {
69    fn get_api_key(&self) -> &str {
70        &self.api_key
71    }
72}
73
74pub trait UnauthenticatedPiHoleAPI {
75    /// Get statistics in a raw format (no number format)
76    fn get_summary_raw(&self) -> Result<SummaryRaw, errors::APIError>;
77
78    /// Get statistics in a formatted style
79    fn get_summary(&self) -> Result<Summary, errors::APIError>;
80
81    /// Get statistics on the number of domains and ads for each 10 minute period
82    fn get_over_time_data_10_mins(&self) -> Result<OverTimeData, errors::APIError>;
83
84    /// Get the Pi-Hole version.
85    fn get_version(&self) -> Result<u32, errors::APIError>;
86
87    /// Get the detailed Pi-Hole versions for core, FTL and web interface.
88    fn get_versions(&self) -> Result<Versions, errors::APIError>;
89}
90
91fn simple_json_request<T, I, K, V>(
92    host: &str,
93    path_query: &str,
94    params: I,
95) -> Result<T, errors::APIError>
96where
97    T: DeserializeOwned,
98    I: IntoIterator,
99    K: AsRef<str>,
100    V: AsRef<str>,
101    <I as IntoIterator>::Item: Borrow<(K, V)>,
102{
103    let path = format!("{}{}", host, path_query);
104    let response = ureq::get(
105        url::Url::parse_with_params(&path, params)
106            .expect("Invalid URL")
107            .as_str(),
108    )
109    .call()?
110    .into_json()?;
111    Ok(response)
112}
113
114impl<T> UnauthenticatedPiHoleAPI for T
115where
116    T: PiHoleAPIHost,
117{
118    fn get_summary_raw(&self) -> Result<SummaryRaw, errors::APIError> {
119        simple_json_request(self.get_host(), "/admin/api.php?summaryRaw", &NO_PARAMS)
120    }
121
122    fn get_summary(&self) -> Result<Summary, errors::APIError> {
123        simple_json_request(self.get_host(), "/admin/api.php?summary", &NO_PARAMS)
124    }
125
126    fn get_over_time_data_10_mins(&self) -> Result<OverTimeData, errors::APIError> {
127        simple_json_request(
128            self.get_host(),
129            "/admin/api.php?overTimeData10mins",
130            &NO_PARAMS,
131        )
132    }
133
134    /// Get simple PiHole version
135    fn get_version(&self) -> Result<u32, errors::APIError> {
136        let raw_version: Version =
137            simple_json_request(self.get_host(), "/admin/api.php?version", &NO_PARAMS)?;
138        Ok(raw_version.version)
139    }
140
141    /// Get versions of core, FTL and web and if updates are available
142    fn get_versions(&self) -> Result<Versions, errors::APIError> {
143        simple_json_request(self.get_host(), "/admin/api.php?versions", &NO_PARAMS)
144    }
145}
146
147pub trait AuthenticatedPiHoleAPI {
148    /// Get the top domains and ads and the number of queries for each. Limit the number of items with `count`.
149    fn get_top_items(&self, count: &Option<u32>) -> Result<TopItems, errors::APIError>;
150
151    /// Get the top clients and the number of queries for each. Limit the number of items with `count`.
152    fn get_top_clients(&self, count: &Option<u32>) -> Result<TopClients, errors::APIError>;
153
154    /// Get the top clients blocked and the number of queries for each. Limit the number of items with `count`.
155    fn get_top_clients_blocked(
156        &self,
157        count: Option<u32>,
158    ) -> Result<TopClientsBlocked, errors::APIError>;
159
160    /// Get the percentage of queries forwarded to each target.
161    fn get_forward_destinations(
162        &self,
163        unsorted: bool,
164    ) -> Result<ForwardDestinations, errors::APIError>;
165
166    /// Get the number of queries per type.
167    fn get_query_types(&self) -> Result<QueryTypes, errors::APIError>;
168
169    /// Get all DNS query data. Limit the number of items with `count`.
170    fn get_all_queries(&self, count: u32) -> Result<Vec<Query>, errors::APIError>;
171
172    /// Enable the Pi-Hole.
173    fn enable(&self) -> Result<Status, errors::APIError>;
174
175    /// Disable the Pi-Hole for `seconds` seconds.
176    fn disable(&self, seconds: u64) -> Result<Status, errors::APIError>;
177
178    /// Get statistics about the DNS cache.
179    fn get_cache_info(&self) -> Result<CacheInfo, errors::APIError>;
180
181    /// Get hostname and IP for hosts
182    fn get_client_names(&self) -> Result<Vec<ClientName>, errors::APIError>;
183
184    /// Get queries by client over time. Maps timestamp to the number of queries by clients.
185    /// Order of clients in the Vector is the same as for get_client_names
186    fn get_over_time_data_clients(&self) -> Result<HashMap<String, Vec<u64>>, errors::APIError>;
187
188    /// Get information about network clients.
189    fn get_network(&self) -> Result<Network, errors::APIError>;
190
191    /// Get the total number of queries received.
192    fn get_queries_count(&self) -> Result<u64, errors::APIError>;
193
194    /// Add domains to a custom white/blacklist.
195    /// Acceptable lists are: `white`, `black`, `white_regex`, `black_regex`, `white_wild`, `black_wild`, `audit`.
196    fn list_add(
197        &self,
198        domain: &str,
199        list: &str,
200    ) -> Result<ListModificationResponse, errors::APIError>;
201
202    /// Remove domain to a custom white/blacklist.
203    /// Acceptable lists are: `white`, `black`, `white_regex`, `black_regex`, `white_wild`, `black_wild`, `audit`.
204    fn list_remove(
205        &self,
206        domain: &str,
207        list: &str,
208    ) -> Result<ListModificationResponse, errors::APIError>;
209
210    /// Get a list of domains on a particular custom white/blacklist
211    /// Acceptable lists are: `white`, `black`, `white_regex`, `black_regex`, `white_wild`, `black_wild`, `audit`.
212    fn list_get_domains(
213        &self,
214        list: &str,
215    ) -> Result<Vec<CustomListDomainDetails>, errors::APIError>;
216
217    /// Get a list of custom DNS records
218    fn get_custom_dns_records(&self) -> Result<Vec<CustomDNSRecord>, errors::APIError>;
219
220    /// Add a custom DNS record
221    fn add_custom_dns_record(
222        &self,
223        ip: &IpAddr,
224        domain: &str,
225    ) -> Result<ListModificationResponse, errors::APIError>;
226
227    /// Delete a custom DNS record
228    fn delete_custom_dns_record(
229        &self,
230        ip: &IpAddr,
231        domain: &str,
232    ) -> Result<ListModificationResponse, errors::APIError>;
233
234    /// Get a list of custom CNAME records
235    fn get_custom_cname_records(&self) -> Result<Vec<CustomCNAMERecord>, errors::APIError>;
236
237    /// Add a custom CNAME record
238    fn add_custom_cname_record(
239        &self,
240        domain: &str,
241        target_domain: &str,
242    ) -> Result<ListModificationResponse, errors::APIError>;
243
244    /// Delete a custom CNAME record
245    fn delete_custom_cname_record(
246        &self,
247        domain: &str,
248        target_domain: &str,
249    ) -> Result<ListModificationResponse, errors::APIError>;
250
251    /// Get max logage
252    fn get_max_logage(&self) -> Result<f32, errors::APIError>;
253}
254
255fn authenticated_json_request<'a, T, I, K, V>(
256    host: &str,
257    path_query: &str,
258    params: I,
259    api_key: &'a str,
260) -> Result<T, errors::APIError>
261where
262    T: DeserializeOwned,
263    I: IntoIterator<Item = (K, V)>,
264    K: AsRef<str>,
265    V: AsRef<str>,
266    // <I as IntoIterator>::Item: Borrow<(K, V)>,
267{
268    let path = format!("{}{}", host, path_query);
269    let auth_params = [("auth".to_string(), api_key.to_string())];
270    let converted_params: Vec<(String, String)> = params
271        .into_iter()
272        .map(|(k, v)| (k.as_ref().to_string(), v.as_ref().to_string()))
273        .collect();
274    let url = url::Url::parse_with_params(&path, converted_params.iter().chain(auth_params.iter()))
275        .expect("Invalid URL");
276    let response_text = ureq::get(url.as_str()).call()?.into_string()?;
277    errors::detect_response_errors(&response_text)?;
278    match serde_json::from_str::<T>(&response_text) {
279        Ok(response) => Ok(response),
280        Err(error) => Err(error.into()),
281    }
282}
283
284impl<T> AuthenticatedPiHoleAPI for T
285where
286    T: PiHoleAPIHost + PiHoleAPIKey,
287{
288    fn get_top_items(&self, count: &Option<u32>) -> Result<TopItems, errors::APIError> {
289        authenticated_json_request(
290            self.get_host(),
291            "/admin/api.php",
292            [("topItems", count.unwrap_or(10).to_string())],
293            self.get_api_key(),
294        )
295    }
296
297    fn get_top_clients(&self, count: &Option<u32>) -> Result<TopClients, errors::APIError> {
298        authenticated_json_request(
299            self.get_host(),
300            "/admin/api.php?",
301            [("topClients", count.unwrap_or(10).to_string())],
302            self.get_api_key(),
303        )
304    }
305
306    fn get_top_clients_blocked(
307        &self,
308        count: Option<u32>,
309    ) -> Result<TopClientsBlocked, errors::APIError> {
310        authenticated_json_request(
311            self.get_host(),
312            "/admin/api.php?",
313            [("topClientsBlocked", count.unwrap_or(10).to_string())],
314            self.get_api_key(),
315        )
316    }
317
318    fn get_forward_destinations(
319        &self,
320        unsorted: bool,
321    ) -> Result<ForwardDestinations, errors::APIError> {
322        let param_value = if unsorted { "unsorted" } else { "" };
323        authenticated_json_request(
324            self.get_host(),
325            "/admin/api.php",
326            [("getForwardDestinations", param_value)],
327            self.get_api_key(),
328        )
329    }
330
331    fn get_query_types(&self) -> Result<QueryTypes, errors::APIError> {
332        authenticated_json_request(
333            self.get_host(),
334            "/admin/api.php",
335            [("getQueryTypes", "")],
336            self.get_api_key(),
337        )
338    }
339
340    fn get_all_queries(&self, count: u32) -> Result<Vec<Query>, errors::APIError> {
341        let mut raw_data: HashMap<String, Vec<Query>> = authenticated_json_request(
342            self.get_host(),
343            "/admin/api.php",
344            [("getAllQueries", count.to_string())],
345            self.get_api_key(),
346        )?;
347        Ok(raw_data.remove("data").unwrap())
348    }
349
350    fn enable(&self) -> Result<Status, errors::APIError> {
351        authenticated_json_request(
352            self.get_host(),
353            "/admin/api.php?",
354            [("enable", "")],
355            self.get_api_key(),
356        )
357    }
358
359    fn disable(&self, seconds: u64) -> Result<Status, errors::APIError> {
360        authenticated_json_request(
361            self.get_host(),
362            "/admin/api.php",
363            [("disable", seconds.to_string())],
364            self.get_api_key(),
365        )
366    }
367
368    fn get_cache_info(&self) -> Result<CacheInfo, errors::APIError> {
369        let mut raw_data: HashMap<String, CacheInfo> = authenticated_json_request(
370            self.get_host(),
371            "/admin/api.php",
372            [("getCacheInfo", "")],
373            self.get_api_key(),
374        )?;
375        Ok(raw_data.remove("cacheinfo").expect("Missing cache info"))
376    }
377
378    fn get_client_names(&self) -> Result<Vec<ClientName>, errors::APIError> {
379        let mut raw_data: HashMap<String, Vec<ClientName>> = authenticated_json_request(
380            self.get_host(),
381            "/admin/api.php",
382            [("getClientNames", "")],
383            self.get_api_key(),
384        )?;
385        Ok(raw_data
386            .remove("clients")
387            .expect("Missing clients attribute"))
388    }
389
390    fn get_over_time_data_clients(&self) -> Result<HashMap<String, Vec<u64>>, errors::APIError> {
391        let mut raw_data: HashMap<String, FakeHashMap<String, Vec<u64>>> =
392            authenticated_json_request(
393                self.get_host(),
394                "/admin/api.php",
395                [("overTimeDataClients", "")],
396                self.get_api_key(),
397            )?;
398
399        Ok(raw_data
400            .remove("over_time")
401            .expect("Missing over_time attribute")
402            .into())
403    }
404
405    fn get_network(&self) -> Result<Network, errors::APIError> {
406        authenticated_json_request(
407            self.get_host(),
408            "/admin/api_db.php",
409            [("network", "")],
410            self.get_api_key(),
411        )
412    }
413
414    fn get_queries_count(&self) -> Result<u64, errors::APIError> {
415        let raw_data: HashMap<String, u64> = authenticated_json_request(
416            self.get_host(),
417            "/admin/api_db.php",
418            [("getQueriesCount", "")],
419            self.get_api_key(),
420        )?;
421        Ok(*raw_data.get("count").expect("Missing count attribute"))
422    }
423
424    fn list_add(
425        &self,
426        domain: &str,
427        list: &str,
428    ) -> Result<ListModificationResponse, errors::APIError> {
429        authenticated_json_request(
430            self.get_host(),
431            "/admin/api.php",
432            [("add", domain), ("list", list)],
433            self.get_api_key(),
434        )
435    }
436
437    fn list_remove(
438        &self,
439        domain: &str,
440        list: &str,
441    ) -> Result<ListModificationResponse, errors::APIError> {
442        authenticated_json_request(
443            self.get_host(),
444            "/admin/api.php",
445            [("sub", domain), ("list", list)],
446            self.get_api_key(),
447        )
448    }
449
450    fn list_get_domains(
451        &self,
452        list: &str,
453    ) -> Result<Vec<CustomListDomainDetails>, errors::APIError> {
454        // if not "add" or "sub", api.php defaults to the "get_domains" action
455        let mut raw_data: HashMap<String, Vec<CustomListDomainDetails>> =
456            authenticated_json_request(
457                self.get_host(),
458                "/admin/api.php",
459                [("get", ""), ("list", list)],
460                self.get_api_key(),
461            )?;
462        Ok(raw_data.remove("data").unwrap())
463    }
464
465    fn get_custom_dns_records(&self) -> Result<Vec<CustomDNSRecord>, errors::APIError> {
466        let mut raw_data: HashMap<String, Vec<Vec<String>>> = authenticated_json_request(
467            self.get_host(),
468            "/admin/api.php",
469            [("customdns", ""), ("action", "get")],
470            self.get_api_key(),
471        )?;
472
473        Ok(raw_data
474            .remove("data")
475            .unwrap()
476            .into_iter()
477            .map(|list_record| CustomDNSRecord {
478                domain: list_record[0].clone(),
479                ip_address: list_record[1].parse().unwrap(),
480            })
481            .collect())
482    }
483
484    fn add_custom_dns_record(
485        &self,
486        ip: &IpAddr,
487        domain: &str,
488    ) -> Result<ListModificationResponse, errors::APIError> {
489        authenticated_json_request(
490            self.get_host(),
491            "/admin/api.php",
492            [
493                ("customdns", ""),
494                ("action", "add"),
495                ("ip", &ip.to_string()),
496                ("domain", domain),
497            ],
498            self.get_api_key(),
499        )
500    }
501
502    fn delete_custom_dns_record(
503        &self,
504        ip: &IpAddr,
505        domain: &str,
506    ) -> Result<ListModificationResponse, errors::APIError> {
507        authenticated_json_request(
508            self.get_host(),
509            "/admin/api.php",
510            [
511                ("customdns", ""),
512                ("action", "delete"),
513                ("ip", &ip.to_string()),
514                ("domain", domain),
515            ],
516            self.get_api_key(),
517        )
518    }
519
520    fn get_custom_cname_records(&self) -> Result<Vec<CustomCNAMERecord>, errors::APIError> {
521        let mut raw_data: HashMap<String, Vec<Vec<String>>> = authenticated_json_request(
522            self.get_host(),
523            "/admin/api.php",
524            [("customcname", ""), ("action", "get")],
525            self.get_api_key(),
526        )?;
527
528        Ok(raw_data
529            .remove("data")
530            .unwrap()
531            .into_iter()
532            .map(|list_record| CustomCNAMERecord {
533                domain: list_record[0].clone(),
534                target_domain: list_record[1].clone(),
535            })
536            .collect())
537    }
538
539    fn add_custom_cname_record(
540        &self,
541        domain: &str,
542        target_domain: &str,
543    ) -> Result<ListModificationResponse, errors::APIError> {
544        authenticated_json_request(
545            self.get_host(),
546            "/admin/api.php",
547            [
548                ("customcname", ""),
549                ("action", "add"),
550                ("domain", domain),
551                ("target", target_domain),
552            ],
553            self.get_api_key(),
554        )
555    }
556
557    fn delete_custom_cname_record(
558        &self,
559        domain: &str,
560        target_domain: &str,
561    ) -> Result<ListModificationResponse, errors::APIError> {
562        authenticated_json_request(
563            self.get_host(),
564            "/admin/api.php",
565            [
566                ("customcname", ""),
567                ("action", "delete"),
568                ("domain", domain),
569                ("target", target_domain),
570            ],
571            self.get_api_key(),
572        )
573    }
574
575    fn get_max_logage(&self) -> Result<f32, errors::APIError> {
576        let mut raw_data: HashMap<String, f32> = authenticated_json_request(
577            self.get_host(),
578            "/admin/api.php",
579            [("getMaxlogage", "")],
580            self.get_api_key(),
581        )?;
582        Ok(raw_data.remove("maxlogage").unwrap())
583    }
584}