Attempting to add support for Pi-hole v6.

This commit is contained in:
Keith Carichner Jr 2025-02-19 17:30:14 -05:00
parent d4565acfe7
commit 38d3d11571
2 changed files with 144 additions and 8 deletions

View file

@ -1786,8 +1786,14 @@ Only required when using AdGuard Home. The username used to log into the admin d
##### `password`
Only required when using AdGuard Home. The password used to log into the admin dashboard. Can be specified from an environment variable using the syntax `${VARIABLE_NAME}`.
##### `token`
Only required when using Pi-hole. The API token which can be found in `Settings -> API -> Show API token`. Can be specified from an environment variable using the syntax `${VARIABLE_NAME}`.
##### `token` (Deprecated)
Only required when using Pi-hole major version 5 or earlier. The API token which can be found in `Settings -> API -> Show API token`. Can be specified from an environment variable using the syntax `${VARIABLE_NAME}`.
##### `app-password`
Only required when using Pi-hole. The App Password can be found in `Settings -> Web Interface / API -> Configure app password`.
##### `version`
Only required if using an older version of PiHole (major version 5 or earlier).
##### `hide-graph`
Whether to hide the graph showing the number of queries over time.

View file

@ -1,17 +1,23 @@
package glance
import (
"bytes"
"context"
"encoding/json"
"errors"
"html/template"
"io"
"log/slog"
"net/http"
"os"
"sort"
"strings"
"time"
)
// Global HTTP client for reuse
var httpClient = &http.Client{}
var dnsStatsWidgetTemplate = mustParseTemplate("dns-stats.html", "widget-base.html")
type dnsStatsWidget struct {
@ -27,6 +33,8 @@ type dnsStatsWidget struct {
AllowInsecure bool `yaml:"allow-insecure"`
URL string `yaml:"url"`
Token string `yaml:"token"`
AppPassword string `yaml:"app-password"`
PiHoleVersion string `yaml:"pihole-version"`
Username string `yaml:"username"`
Password string `yaml:"password"`
}
@ -62,7 +70,7 @@ func (widget *dnsStatsWidget) update(ctx context.Context) {
if widget.Service == "adguard" {
stats, err = fetchAdguardStats(widget.URL, widget.AllowInsecure, widget.Username, widget.Password, widget.HideGraph)
} else {
stats, err = fetchPiholeStats(widget.URL, widget.AllowInsecure, widget.Token, widget.HideGraph)
stats, err = fetchPiholeStats(widget.URL, widget.AllowInsecure, widget.Token, widget.HideGraph, widget.PiHoleVersion, widget.AppPassword)
}
if !widget.canContinueUpdateAfterHandlingErr(err) {
@ -275,13 +283,135 @@ func (p *piholeTopBlockedDomains) UnmarshalJSON(data []byte) error {
return nil
}
func fetchPiholeStats(instanceURL string, allowInsecure bool, token string, noGraph bool) (*dnsStats, error) {
if token == "" {
return nil, errors.New("missing API token")
// piholeGetSID retrieves a new SID from Pi-hole using the app password.
func piholeGetSID(instanceURL, appPassword string) (string, error) {
requestURL := strings.TrimRight(instanceURL, "/") + "/api/auth"
requestBody := []byte(`{"password":"` + appPassword + `"}`)
request, err := http.NewRequest("POST", requestURL, bytes.NewBuffer(requestBody))
if err != nil {
return "", errors.New("failed to create authentication request: " + err.Error())
}
request.Header.Set("Content-Type", "application/json")
response, err := httpClient.Do(request)
if err != nil {
return "", errors.New("failed to send authentication request: " + err.Error())
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return "", errors.New("authentication failed, received status: " + response.Status)
}
requestURL := strings.TrimRight(instanceURL, "/") +
"/admin/api.php?summaryRaw&topItems&overTimeData10mins&auth=" + token
body, err := io.ReadAll(response.Body)
if err != nil {
return "", errors.New("failed to read authentication response: " + err.Error())
}
var jsonResponse struct {
Session struct {
SID string `json:"sid"`
} `json:"session"`
}
if err := json.Unmarshal(body, &jsonResponse); err != nil {
return "", errors.New("failed to parse authentication response: " + err.Error())
}
if jsonResponse.Session.SID == "" {
return "", errors.New("authentication response did not contain a valid SID")
}
return jsonResponse.Session.SID, nil
}
// piholeCheckAndRefreshSID ensures the SID is valid, refreshing it if necessary.
func piholeCheckAndRefreshSID(instanceURL, appPassword string) (string, error) {
sid := os.Getenv("SID")
if sid == "" {
newSID, err := piholeGetSID(instanceURL, appPassword)
if err != nil {
return "", err
}
os.Setenv("SID", newSID)
return newSID, nil
}
requestURL := strings.TrimRight(instanceURL, "/") + "/api/auth?sid=" + sid
requestBody := []byte(`{"password":"` + appPassword + `"}`)
request, err := http.NewRequest("GET", requestURL, bytes.NewBuffer(requestBody))
if err != nil {
return "", errors.New("failed to create SID validation request: " + err.Error())
}
request.Header.Set("Content-Type", "application/json")
response, err := httpClient.Do(request)
if err != nil {
return "", errors.New("failed to send SID validation request: " + err.Error())
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
// Fetch a new SID if validation request fails
newSID, err := piholeGetSID(instanceURL, appPassword)
if err != nil {
return "", err
}
os.Setenv("SID", newSID)
return newSID, nil
}
body, err := io.ReadAll(response.Body)
if err != nil {
return "", errors.New("failed to read SID validation response: " + err.Error())
}
var jsonResponse struct {
Session struct {
Valid bool `json:"valid"`
SID string `json:"sid"`
} `json:"session"`
}
if err := json.Unmarshal(body, &jsonResponse); err != nil {
return "", errors.New("failed to parse SID validation response: " + err.Error())
}
if !jsonResponse.Session.Valid {
newSID, err := piholeGetSID(instanceURL, appPassword)
if err != nil {
return "", err
}
os.Setenv("SID", newSID)
return newSID, nil
}
return sid, nil
}
func fetchPiholeStats(instanceURL string, allowInsecure bool, token string, noGraph bool, version, appPassword string) (*dnsStats, error) {
var requestURL string
// Handle Pi-hole v6 authentication
if version == "" || version == "6" {
if appPassword == "" {
return nil, errors.New("missing app password")
}
sid, err := piholeCheckAndRefreshSID(instanceURL, appPassword)
if err != nil {
return nil, err
}
requestURL = strings.TrimRight(instanceURL, "/") + "/api/stats/summary?sid=" + sid
} else {
if token == "" {
return nil, errors.New("missing API token")
}
requestURL = strings.TrimRight(instanceURL, "/") + "/admin/api.php?summaryRaw&topItems&overTimeData10mins&auth=" + token
}
request, err := http.NewRequest("GET", requestURL, nil)
if err != nil {