From 29ac7bfea7af2e19af38c88f9c583be1806abbb0 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Sun, 31 Mar 2024 20:34:46 -0700 Subject: [PATCH] Feature: Support pi-hole v6 (#3207) --- docs/widgets/services/pihole.md | 1 + src/utils/config/service-helpers.js | 8 ++- src/widgets/pihole/component.jsx | 2 +- src/widgets/pihole/proxy.js | 95 +++++++++++++++++++++++++++++ src/widgets/pihole/widget.js | 14 ++--- 5 files changed, 107 insertions(+), 13 deletions(-) create mode 100644 src/widgets/pihole/proxy.js diff --git a/docs/widgets/services/pihole.md b/docs/widgets/services/pihole.md index 8079d1b1f..90d5926c3 100644 --- a/docs/widgets/services/pihole.md +++ b/docs/widgets/services/pihole.md @@ -15,6 +15,7 @@ Note: by default the "blocked" and "blocked_percent" fields are merged e.g. "1,2 widget: type: pihole url: http://pi.hole.or.ip + version: 6 # required if running v6 or higher, defaults to 5 key: yourpiholeapikey # optional ``` diff --git a/src/utils/config/service-helpers.js b/src/utils/config/service-helpers.js index bee7db4e2..bea282782 100644 --- a/src/utils/config/service-helpers.js +++ b/src/utils/config/service-helpers.js @@ -393,8 +393,10 @@ export function cleanServiceGroups(groups) { enableBlocks, enableNowPlaying, - // glances + // glances, pihole version, + + // glances chart, metric, pointsLimit, @@ -528,8 +530,10 @@ export function cleanServiceGroups(groups) { if (snapshotHost) cleanedService.widget.snapshotHost = snapshotHost; if (snapshotPath) cleanedService.widget.snapshotPath = snapshotPath; } - if (type === "glances") { + if (["glances", "pihole"].includes(type)) { if (version) cleanedService.widget.version = version; + } + if (type === "glances") { if (metric) cleanedService.widget.metric = metric; if (chart !== undefined) { cleanedService.widget.chart = chart; diff --git a/src/widgets/pihole/component.jsx b/src/widgets/pihole/component.jsx index a36071a1b..4d95b4ace 100644 --- a/src/widgets/pihole/component.jsx +++ b/src/widgets/pihole/component.jsx @@ -9,7 +9,7 @@ export default function Component({ service }) { const { widget } = service; - const { data: piholeData, error: piholeError } = useWidgetAPI(widget, "summaryRaw"); + const { data: piholeData, error: piholeError } = useWidgetAPI(widget); if (piholeError) { return ; diff --git a/src/widgets/pihole/proxy.js b/src/widgets/pihole/proxy.js new file mode 100644 index 000000000..724d4943a --- /dev/null +++ b/src/widgets/pihole/proxy.js @@ -0,0 +1,95 @@ +import cache from "memory-cache"; + +import { httpProxy } from "utils/proxy/http"; +import { formatApiCall } from "utils/proxy/api-helpers"; +import getServiceWidget from "utils/config/service-helpers"; +import createLogger from "utils/logger"; +import widgets from "widgets/widgets"; + +const proxyName = "piholeProxyHandler"; +const logger = createLogger(proxyName); +const sessionSIDCacheKey = `${proxyName}__sessionSID`; + +async function login(widget, service) { + const url = formatApiCall(widgets[widget.type].api, { ...widget, endpoint: "auth" }); + const [status, , data] = await httpProxy(url, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + password: widget.key, + }), + }); + + const dataParsed = JSON.parse(data); + + if (status !== 200 || !dataParsed.session) { + logger.error("Failed to login to Pi-Hole API, status: %d", status); + cache.del(`${sessionSIDCacheKey}.${service}`); + } else { + cache.put(`${sessionSIDCacheKey}.${service}`, dataParsed.session.sid, dataParsed.session.validity); + } +} + +export default async function piholeProxyHandler(req, res) { + const { group, service } = req.query; + let endpoint = "stats/summary"; + + if (!group || !service) { + logger.error("Invalid or missing service '%s' or group '%s'", service, group); + return res.status(400).json({ error: "Invalid proxy service type" }); + } + + const widget = await getServiceWidget(group, service); + if (!widget) { + logger.error("Invalid or missing widget for service '%s' in group '%s'", service, group); + return res.status(400).json({ error: "Invalid widget configuration" }); + } + + let status; + let data; + if (!widget.version || widget.version < 6) { + // pihole v5 + endpoint = "summaryRaw"; + [status, , data] = await httpProxy(formatApiCall(widgets[widget.type].apiv5, { ...widget, endpoint })); + return res.status(status).send(data); + } + + // pihole v6 + if (!cache.get(`${sessionSIDCacheKey}.${service}`)) { + await login(widget, service); + } + + const sid = cache.get(`${sessionSIDCacheKey}.${service}`); + if (!sid) { + return res.status(500).json({ error: "Failed to authenticate with Pi-hole" }); + } + + try { + logger.debug("Calling Pi-hole API endpoint: %s", endpoint); + + [status, , data] = await httpProxy(formatApiCall(widgets[widget.type].api, { ...widget, endpoint }), { + headers: { + "Content-Type": "application/json", + "X-FTL-SID": sid, + }, + }); + + if (status !== 200) { + logger.error("Error calling Pi-Hole API: %d. Data: %s", status, data); + return res.status(status).json({ error: "Pi-Hole API Error", data }); + } + + const dataParsed = JSON.parse(data); + return res.status(status).json({ + domains_being_blocked: dataParsed.gravity.domains_being_blocked, + ads_blocked_today: dataParsed.queries.blocked, + ads_percentage_today: dataParsed.queries.percent_blocked, + dns_queries_today: dataParsed.queries.total, + }); + } catch (error) { + logger.error("Exception calling Pi-Hole API: %s", error.message); + return res.status(500).json({ error: "Pi-Hole API Error", message: error.message }); + } +} diff --git a/src/widgets/pihole/widget.js b/src/widgets/pihole/widget.js index 10b30b1ab..54c848326 100644 --- a/src/widgets/pihole/widget.js +++ b/src/widgets/pihole/widget.js @@ -1,15 +1,9 @@ -import genericProxyHandler from "utils/proxy/handlers/generic"; +import piholeProxyHandler from "./proxy"; const widget = { - api: "{url}/admin/api.php?{endpoint}&auth={key}", - proxyHandler: genericProxyHandler, - - mappings: { - summaryRaw: { - endpoint: "summaryRaw", - validate: ["dns_queries_today", "ads_blocked_today", "ads_percentage_today", "domains_being_blocked"], - }, - }, + api: "{url}/api/{endpoint}", + apiv5: "{url}/admin/api.php?{endpoint}&auth={key}", + proxyHandler: piholeProxyHandler, }; export default widget;