Attempting to add support for Pi-hole v6.
This commit is contained in:
parent
d4565acfe7
commit
38d3d11571
2 changed files with 144 additions and 8 deletions
|
@ -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.
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Add table
Reference in a new issue