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#[derive(Debug)]
25pub struct PiHoleAPIConfig {
26 host: String,
28}
29
30impl PiHoleAPIConfig {
31 pub fn new(host: String) -> Self {
34 Self { host }
35 }
36}
37
38#[derive(Debug)]
40pub struct PiHoleAPIConfigWithKey {
41 host: String,
43
44 api_key: String,
46}
47
48impl PiHoleAPIConfigWithKey {
49 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 fn get_summary_raw(&self) -> Result<SummaryRaw, errors::APIError>;
77
78 fn get_summary(&self) -> Result<Summary, errors::APIError>;
80
81 fn get_over_time_data_10_mins(&self) -> Result<OverTimeData, errors::APIError>;
83
84 fn get_version(&self) -> Result<u32, errors::APIError>;
86
87 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 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 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 fn get_top_items(&self, count: &Option<u32>) -> Result<TopItems, errors::APIError>;
150
151 fn get_top_clients(&self, count: &Option<u32>) -> Result<TopClients, errors::APIError>;
153
154 fn get_top_clients_blocked(
156 &self,
157 count: Option<u32>,
158 ) -> Result<TopClientsBlocked, errors::APIError>;
159
160 fn get_forward_destinations(
162 &self,
163 unsorted: bool,
164 ) -> Result<ForwardDestinations, errors::APIError>;
165
166 fn get_query_types(&self) -> Result<QueryTypes, errors::APIError>;
168
169 fn get_all_queries(&self, count: u32) -> Result<Vec<Query>, errors::APIError>;
171
172 fn enable(&self) -> Result<Status, errors::APIError>;
174
175 fn disable(&self, seconds: u64) -> Result<Status, errors::APIError>;
177
178 fn get_cache_info(&self) -> Result<CacheInfo, errors::APIError>;
180
181 fn get_client_names(&self) -> Result<Vec<ClientName>, errors::APIError>;
183
184 fn get_over_time_data_clients(&self) -> Result<HashMap<String, Vec<u64>>, errors::APIError>;
187
188 fn get_network(&self) -> Result<Network, errors::APIError>;
190
191 fn get_queries_count(&self) -> Result<u64, errors::APIError>;
193
194 fn list_add(
197 &self,
198 domain: &str,
199 list: &str,
200 ) -> Result<ListModificationResponse, errors::APIError>;
201
202 fn list_remove(
205 &self,
206 domain: &str,
207 list: &str,
208 ) -> Result<ListModificationResponse, errors::APIError>;
209
210 fn list_get_domains(
213 &self,
214 list: &str,
215 ) -> Result<Vec<CustomListDomainDetails>, errors::APIError>;
216
217 fn get_custom_dns_records(&self) -> Result<Vec<CustomDNSRecord>, errors::APIError>;
219
220 fn add_custom_dns_record(
222 &self,
223 ip: &IpAddr,
224 domain: &str,
225 ) -> Result<ListModificationResponse, errors::APIError>;
226
227 fn delete_custom_dns_record(
229 &self,
230 ip: &IpAddr,
231 domain: &str,
232 ) -> Result<ListModificationResponse, errors::APIError>;
233
234 fn get_custom_cname_records(&self) -> Result<Vec<CustomCNAMERecord>, errors::APIError>;
236
237 fn add_custom_cname_record(
239 &self,
240 domain: &str,
241 target_domain: &str,
242 ) -> Result<ListModificationResponse, errors::APIError>;
243
244 fn delete_custom_cname_record(
246 &self,
247 domain: &str,
248 target_domain: &str,
249 ) -> Result<ListModificationResponse, errors::APIError>;
250
251 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 {
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 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}