Merge pull request #37 from shaned24/master

feat(ApiTokenAuth): Add support for using PiHole's api token
This commit is contained in:
Vincent Composieux 2020-04-27 10:54:57 +02:00 committed by GitHub
commit a124a6be35
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 64 additions and 22 deletions

View file

@ -49,6 +49,18 @@ $ docker run \
ekofr/pihole-exporter:latest ekofr/pihole-exporter:latest
``` ```
Or use PiHole's `WEBPASSWORD` as an API token instead of the password
```bash
$ API_TOKEN=$(awk -F= -v key="WEBPASSWORD" '$1==key {print $2}' /etc/pihole/setupVars.conf)
$ docker run \
-e 'PIHOLE_HOSTNAME=192.168.1.2' \
-e "PIHOLE_API_TOKEN=$API_TOKEN" \
-e 'INTERVAL=30s' \
-e 'PORT=9617' \
ekofr/pihole-exporter:latest
```
### From sources ### From sources
Optionally, you can download and build it from the sources. You have to retrieve the project sources by using one of the following way: Optionally, you can download and build it from the sources. You have to retrieve the project sources by using one of the following way:
@ -73,9 +85,20 @@ $ GOOS=linux GOARCH=arm GOARM=7 go build -o pihole_exporter .
In order to run the exporter, type the following command (arguments are optional): In order to run the exporter, type the following command (arguments are optional):
Using a password
```bash ```bash
$ ./pihole_exporter -pihole_hostname 192.168.1.10 -pihole_password azerty $ ./pihole_exporter -pihole_hostname 192.168.1.10 -pihole_password azerty
```
Or use PiHole's `WEBPASSWORD` as an API token instead of the password
```bash
$ API_TOKEN=$(awk -F= -v key="WEBPASSWORD" '$1==key {print $2}' /etc/pihole/setupVars.conf)
$ ./pihole_exporter -pihole_hostname 192.168.1.10 -pihole_api_token $API_TOKEN
```
```bash
2019/05/09 20:19:52 ------------------------------------ 2019/05/09 20:19:52 ------------------------------------
2019/05/09 20:19:52 - PI-Hole exporter configuration - 2019/05/09 20:19:52 - PI-Hole exporter configuration -
2019/05/09 20:19:52 ------------------------------------ 2019/05/09 20:19:52 ------------------------------------
@ -126,6 +149,10 @@ scrape_configs:
# Password defined on the PI-Hole interface # Password defined on the PI-Hole interface
-pihole_password string (optional) -pihole_password string (optional)
# WEBPASSWORD / api token defined on the PI-Hole interface at `/etc/pihole/setupVars.conf`
-pihole_api_token string (optional)
# Port to be used for the exporter # Port to be used for the exporter
-port string (optional) (default "9617") -port string (optional) (default "9617")
``` ```

View file

@ -15,20 +15,20 @@ import (
// Config is the exporter CLI configuration. // Config is the exporter CLI configuration.
type Config struct { type Config struct {
PIHoleHostname string `config:"pihole_hostname"` PIHoleHostname string `config:"pihole_hostname"`
PIHolePassword string `config:"pihole_password"` PIHolePassword string `config:"pihole_password"`
PIHoleApiToken string `config:"pihole_api_token"`
Port string `config:"port"` Port string `config:"port"`
Interval time.Duration `config:"interval"` Interval time.Duration `config:"interval"`
} }
func getDefaultConfig() *Config { func getDefaultConfig() *Config {
return &Config{ return &Config{
PIHoleHostname: "127.0.0.1", PIHoleHostname: "127.0.0.1",
PIHolePassword: "", PIHolePassword: "",
PIHoleApiToken: "",
Port: "9617", Port: "9617",
Interval: 10 * time.Second, Interval: 10 * time.Second,
} }
} }
@ -61,10 +61,18 @@ func (c Config) show() {
valueField := val.Field(i) valueField := val.Field(i)
typeField := val.Type().Field(i) typeField := val.Type().Field(i)
// Do not print password // Do not print password or api token but do print the authentication method
if typeField.Name != "PIHolePassword" { if typeField.Name != "PIHolePassword" && typeField.Name != "PIHoleApiToken" {
log.Println(fmt.Sprintf("%s : %v", typeField.Name, valueField.Interface())) log.Println(fmt.Sprintf("%s : %v", typeField.Name, valueField.Interface()))
} else {
showAuthenticationMethod(typeField.Name, valueField.String())
} }
} }
log.Println("------------------------------------") log.Println("------------------------------------")
} }
func showAuthenticationMethod(name, value string) {
if len(value) > 0 {
log.Println(fmt.Sprintf("Pi-Hole Authentication Method : %s", name))
}
}

View file

@ -26,13 +26,15 @@ type Client struct {
hostname string hostname string
password string password string
sessionID string sessionID string
apiToken string
} }
// NewClient method initializes a new PI-Hole client. // NewClient method initializes a new PI-Hole client.
func NewClient(hostname, password string, interval time.Duration) *Client { func NewClient(hostname, password, apiToken string, interval time.Duration) *Client {
return &Client{ return &Client{
hostname: hostname, hostname: hostname,
password: password, password: password,
apiToken: apiToken,
interval: interval, interval: interval,
httpClient: http.Client{ httpClient: http.Client{
CheckRedirect: func(req *http.Request, via []*http.Request) error { CheckRedirect: func(req *http.Request, via []*http.Request) error {
@ -42,15 +44,12 @@ func NewClient(hostname, password string, interval time.Duration) *Client {
} }
} }
// Scrape method logins and retrieves statistics from PI-Hole JSON API // Scrape method authenticates and retrieves statistics from PI-Hole JSON API
// and then pass them as Prometheus metrics. // and then pass them as Prometheus metrics.
func (c *Client) Scrape() { func (c *Client) Scrape() {
for range time.Tick(c.interval) { for range time.Tick(c.interval) {
if c.isAuthenticated() {
c.sessionID = c.getPHPSessionID()
}
stats := c.getStatistics() stats := c.getStatistics()
c.setMetrics(stats) c.setMetrics(stats)
log.Printf("New tick of statistics: %s", stats.ToString()) log.Printf("New tick of statistics: %s", stats.ToString())
@ -133,12 +132,16 @@ func (c *Client) getStatistics() *Stats {
statsURL := fmt.Sprintf(statsURLPattern, c.hostname) statsURL := fmt.Sprintf(statsURLPattern, c.hostname)
if c.isUsingApiToken() {
statsURL = fmt.Sprintf("%s&auth=%s", statsURL, c.apiToken)
}
req, err := http.NewRequest("GET", statsURL, nil) req, err := http.NewRequest("GET", statsURL, nil)
if err != nil { if err != nil {
log.Fatal("An error has occured when creating HTTP statistics request", err) log.Fatal("An error has occured when creating HTTP statistics request", err)
} }
if c.isAuthenticated() { if c.isUsingPassword() {
c.authenticateRequest(req) c.authenticateRequest(req)
} }
@ -160,11 +163,15 @@ func (c *Client) getStatistics() *Stats {
return &stats return &stats
} }
func (c *Client) isAuthenticated() bool { func (c *Client) isUsingPassword() bool {
return len(c.password) > 0 return len(c.password) > 0
} }
func (c *Client) isUsingApiToken() bool {
return len(c.apiToken) > 0
}
func (c *Client) authenticateRequest(req *http.Request) { func (c *Client) authenticateRequest(req *http.Request) {
cookie := http.Cookie{Name: "PHPSESSID", Value: c.sessionID} cookie := http.Cookie{Name: "PHPSESSID", Value: c.getPHPSessionID()}
req.AddCookie(&cookie) req.AddCookie(&cookie)
} }

View file

@ -26,14 +26,14 @@ func main() {
metrics.Init() metrics.Init()
initPiholeClient(conf.PIHoleHostname, conf.PIHolePassword, conf.Interval) initPiHoleClient(conf.PIHoleHostname, conf.PIHolePassword, conf.PIHoleApiToken, conf.Interval)
initHttpServer(conf.Port) initHttpServer(conf.Port)
handleExitSignal() handleExitSignal()
} }
func initPiholeClient(hostname, password string, interval time.Duration) { func initPiHoleClient(hostname, password, apiToken string, interval time.Duration) {
client := pihole.NewClient(hostname, password, interval) client := pihole.NewClient(hostname, password, apiToken, interval)
go client.Scrape() go client.Scrape()
} }