2019-05-08 23:45:04 +02:00
|
|
|
package pihole
|
|
|
|
|
|
|
|
import (
|
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
|
|
|
"io/ioutil"
|
|
|
|
"log"
|
|
|
|
"net/http"
|
|
|
|
"net/url"
|
|
|
|
"strconv"
|
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/eko/pihole-exporter/internal/metrics"
|
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
|
|
|
loginURLPattern = "http://%s/admin/index.php?login"
|
|
|
|
statsURLPattern = "http://%s/admin/api.php?summaryRaw&overTimeData&topItems&recentItems&getQueryTypes&getForwardDestinations&getQuerySources&jsonForceObject"
|
|
|
|
)
|
|
|
|
|
2019-05-09 21:11:31 +02:00
|
|
|
// Client struct is a PI-Hole client to request an instance of a PI-Hole ad blocker.
|
2019-05-08 23:45:04 +02:00
|
|
|
type Client struct {
|
2019-05-10 21:03:33 +02:00
|
|
|
httpClient http.Client
|
|
|
|
interval time.Duration
|
2019-05-08 23:45:04 +02:00
|
|
|
hostname string
|
|
|
|
password string
|
2019-05-10 21:03:33 +02:00
|
|
|
sessionID string
|
2019-05-08 23:45:04 +02:00
|
|
|
}
|
|
|
|
|
2019-05-09 21:11:31 +02:00
|
|
|
// NewClient method initializes a new PI-Hole client.
|
2019-05-08 23:45:04 +02:00
|
|
|
func NewClient(hostname, password string, interval time.Duration) *Client {
|
|
|
|
return &Client{
|
|
|
|
hostname: hostname,
|
|
|
|
password: password,
|
|
|
|
interval: interval,
|
|
|
|
httpClient: http.Client{
|
|
|
|
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
|
|
|
return http.ErrUseLastResponse
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-05-09 21:11:31 +02:00
|
|
|
// Scrape method logins and retrieves statistics from PI-Hole JSON API
|
|
|
|
// and then pass them as Prometheus metrics.
|
|
|
|
func (c *Client) Scrape() {
|
2019-05-08 23:45:04 +02:00
|
|
|
for range time.Tick(c.interval) {
|
2019-05-10 21:03:33 +02:00
|
|
|
if c.isAuthenticated() {
|
|
|
|
c.sessionID = c.getPHPSessionID()
|
2019-05-08 23:45:04 +02:00
|
|
|
}
|
|
|
|
|
2019-05-10 21:03:33 +02:00
|
|
|
stats := c.getStatistics()
|
2019-05-09 21:11:31 +02:00
|
|
|
c.setMetrics(stats)
|
2019-05-08 23:45:04 +02:00
|
|
|
|
2019-05-09 21:11:31 +02:00
|
|
|
log.Printf("New tick of statistics: %s", stats.ToString())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *Client) setMetrics(stats *Stats) {
|
|
|
|
metrics.DomainsBlocked.WithLabelValues(c.hostname).Set(float64(stats.DomainsBeingBlocked))
|
|
|
|
metrics.DNSQueriesToday.WithLabelValues(c.hostname).Set(float64(stats.DNSQueriesToday))
|
|
|
|
metrics.AdsBlockedToday.WithLabelValues(c.hostname).Set(float64(stats.AdsBlockedToday))
|
|
|
|
metrics.AdsPercentageToday.WithLabelValues(c.hostname).Set(float64(stats.AdsPercentageToday))
|
|
|
|
metrics.UniqueDomains.WithLabelValues(c.hostname).Set(float64(stats.UniqueDomains))
|
|
|
|
metrics.QueriesForwarded.WithLabelValues(c.hostname).Set(float64(stats.QueriesForwarded))
|
|
|
|
metrics.QueriesCached.WithLabelValues(c.hostname).Set(float64(stats.QueriesCached))
|
|
|
|
metrics.ClientsEverSeen.WithLabelValues(c.hostname).Set(float64(stats.ClientsEverSeen))
|
|
|
|
metrics.UniqueClients.WithLabelValues(c.hostname).Set(float64(stats.UniqueClients))
|
|
|
|
metrics.DNSQueriesAllTypes.WithLabelValues(c.hostname).Set(float64(stats.DNSQueriesAllTypes))
|
|
|
|
|
|
|
|
metrics.Reply.WithLabelValues(c.hostname, "no_data").Set(float64(stats.ReplyNoData))
|
|
|
|
metrics.Reply.WithLabelValues(c.hostname, "nx_domain").Set(float64(stats.ReplyNxDomain))
|
|
|
|
metrics.Reply.WithLabelValues(c.hostname, "cname").Set(float64(stats.ReplyCname))
|
|
|
|
metrics.Reply.WithLabelValues(c.hostname, "ip").Set(float64(stats.ReplyIP))
|
|
|
|
|
|
|
|
var isEnabled int = 0
|
|
|
|
if stats.Status == enabledStatus {
|
|
|
|
isEnabled = 1
|
|
|
|
}
|
|
|
|
metrics.Status.WithLabelValues(c.hostname).Set(float64(isEnabled))
|
|
|
|
|
|
|
|
for domain, value := range stats.TopQueries {
|
|
|
|
metrics.TopQueries.WithLabelValues(c.hostname, domain).Set(float64(value))
|
|
|
|
}
|
|
|
|
|
|
|
|
for domain, value := range stats.TopAds {
|
|
|
|
metrics.TopAds.WithLabelValues(c.hostname, domain).Set(float64(value))
|
|
|
|
}
|
|
|
|
|
|
|
|
for source, value := range stats.TopSources {
|
|
|
|
metrics.TopSources.WithLabelValues(c.hostname, source).Set(float64(value))
|
|
|
|
}
|
|
|
|
|
|
|
|
for destination, value := range stats.ForwardDestinations {
|
|
|
|
metrics.ForwardDestinations.WithLabelValues(c.hostname, destination).Set(value)
|
|
|
|
}
|
|
|
|
|
|
|
|
for queryType, value := range stats.QueryTypes {
|
|
|
|
metrics.QueryTypes.WithLabelValues(c.hostname, queryType).Set(value)
|
2019-05-08 23:45:04 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-05-10 21:03:33 +02:00
|
|
|
func (c *Client) getPHPSessionID() (sessionID string) {
|
2019-05-08 23:45:04 +02:00
|
|
|
loginURL := fmt.Sprintf(loginURLPattern, c.hostname)
|
|
|
|
values := url.Values{"pw": []string{c.password}}
|
|
|
|
|
|
|
|
req, err := http.NewRequest("POST", loginURL, strings.NewReader(values.Encode()))
|
|
|
|
if err != nil {
|
|
|
|
log.Fatal("An error has occured when creating HTTP statistics request", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
|
|
|
req.Header.Add("Content-Length", strconv.Itoa(len(values.Encode())))
|
|
|
|
|
|
|
|
resp, err := c.httpClient.Do(req)
|
|
|
|
if err != nil {
|
|
|
|
log.Printf("An error has occured during login to PI-Hole: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, cookie := range resp.Cookies() {
|
|
|
|
if cookie.Name == "PHPSESSID" {
|
|
|
|
sessionID = cookie.Value
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-05-10 21:03:33 +02:00
|
|
|
return
|
2019-05-08 23:45:04 +02:00
|
|
|
}
|
|
|
|
|
2019-05-10 21:03:33 +02:00
|
|
|
func (c *Client) getStatistics() *Stats {
|
2019-05-08 23:45:04 +02:00
|
|
|
var stats Stats
|
|
|
|
|
|
|
|
statsURL := fmt.Sprintf(statsURLPattern, c.hostname)
|
|
|
|
|
|
|
|
req, err := http.NewRequest("GET", statsURL, nil)
|
|
|
|
if err != nil {
|
|
|
|
log.Fatal("An error has occured when creating HTTP statistics request", err)
|
|
|
|
}
|
|
|
|
|
2019-05-10 21:03:33 +02:00
|
|
|
if c.isAuthenticated() {
|
|
|
|
c.authenticateRequest(req)
|
|
|
|
}
|
2019-05-08 23:45:04 +02:00
|
|
|
|
|
|
|
resp, err := c.httpClient.Do(req)
|
|
|
|
if err != nil {
|
|
|
|
log.Println("An error has occured during retrieving PI-Hole statistics", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
body, err := ioutil.ReadAll(resp.Body)
|
|
|
|
if err != nil {
|
|
|
|
log.Println("Unable to read PI-Hole statistics HTTP response", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
err = json.Unmarshal(body, &stats)
|
|
|
|
if err != nil {
|
|
|
|
log.Println("Unable to unmarshal PI-Hole statistics to statistics struct model", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return &stats
|
|
|
|
}
|
2019-05-10 21:03:33 +02:00
|
|
|
|
|
|
|
func (c *Client) isAuthenticated() bool {
|
|
|
|
return len(c.password) > 0
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *Client) authenticateRequest(req *http.Request) {
|
|
|
|
cookie := http.Cookie{Name: "PHPSESSID", Value: c.sessionID}
|
|
|
|
req.AddCookie(&cookie)
|
|
|
|
}
|