Skip to content

Commit

Permalink
Add TLS support and config from environment variables
Browse files Browse the repository at this point in the history
Needs a patched go-http-digest-auth-client, to allow a custom client that skips TLS verify
  • Loading branch information
ndecker committed Dec 11, 2022
1 parent 693e1b8 commit 62d8380
Show file tree
Hide file tree
Showing 9 changed files with 199 additions and 96 deletions.
29 changes: 14 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ This exporter is known to work with the following models:
go install

### Docker

git clone https://github.com/ndecker/fritzbox_exporter/
docker build -t fritzbox_exporter fritzbox_exporter

Expand All @@ -39,21 +40,19 @@ This exporter is known to work with the following models:
In the configuration of the Fritzbox the option "Statusinformationen über UPnP übertragen" in the dialog "Heimnetz >
Heimnetzübersicht > Netzwerkeinstellungen" has to be enabled.

Usage:

./fritzbox_exporter -h
-gateway-address string
The hostname or IP of the FRITZ!Box (default "fritz.box")
-gateway-port int
The port of the FRITZ!Box UPnP service (default 49000)
-listen-address string
The address to listen on for HTTP requests. (default ":9133")
-password string
The password for the FRITZ!Box UPnP service
-test
print all available metrics to stdout
-username string
The user for the FRITZ!Box UPnP service
## Configuration

| command line parameter | environment variable | default | |
|------------------------|---------------------------|-----------|------------------------------------------------|
| -listen-address | FRITZBOX_EXPORTER_LISTEN | :9133 | The address to listen on for HTTP requests |
| -gateway-address | FRITZBOX_DEVICE | fritz.box | The hostname or IP of the FRITZ!Box |
| -gateway-port | FRITZBOX_PORT | 49000 | The port of the FRITZ!Box UPnP service |
| -gateway-port | FRITZBOX_PORT_TLS | 49443 | The port of the FRITZ!Box TLS UPnP service |
| -username | FRITZBOX_USERNAME | | The user for the FRITZ!Box UPnP service |
| -password | FRITZBOX_PASSWORD | | The password for the FRITZ!Box UPnP service |
| -use-tls | FRITZBOX_USE_TLS | true | Use TLS/HTTPS connection to FRITZ!Box |
| -allow-selfsigned | FTITZBOX_ALLOW_SELFSIGNED | true | Allow selfsigned certificate from FRITZ!Box |
| -test | | | Print all available metrics to stdout and exit |


## Prerequisites
Expand Down
49 changes: 37 additions & 12 deletions collector.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package main

import (
"fmt"
"log"
"sync"
"time"

Expand All @@ -18,41 +19,65 @@ var collectErrors = prometheus.NewCounter(prometheus.CounterOpts{

type FritzboxCollector struct {
Parameters upnp.ConnectionParameters
sync.Mutex // protects Root
Root *upnp.Root
sync.Mutex // protects Roots
IGDRoot *upnp.Root
TR64Root *upnp.Root
}

// LoadServices tries to load the service information. Retries until success.
func (fc *FritzboxCollector) LoadServices() {
igdRoot := fc.loadService(upnp.IGDServiceDescriptor)
log.Printf("%d IGD services loaded\n", len(igdRoot.Services))
fc.Lock()
fc.IGDRoot = igdRoot
fc.Unlock()

if fc.Parameters.Username == "" {
log.Printf("no username set: not loading TR64 services")
return
}
tr64Root := fc.loadService(upnp.TR64ServiceDescriptor)
log.Printf("%d TR64 services loaded\n", len(tr64Root.Services))
fc.Lock()
fc.TR64Root = tr64Root
fc.Unlock()
}

func (fc *FritzboxCollector) loadService(desc string) *upnp.Root {
for {
root, err := upnp.LoadServices(fc.Parameters)
root, err := upnp.LoadServiceRoot(fc.Parameters, desc)
if err != nil {
fmt.Printf("cannot load services: %s\n", err)

time.Sleep(serviceLoadRetryTime)
continue
}

fmt.Printf("services loaded\n")

fc.Lock()
fc.Root = root
fc.Unlock()
return
return root
}
}

func (fc *FritzboxCollector) Describe(ch chan<- *prometheus.Desc) {
for _, m := range metrics {
for _, m := range IGDMetrics {
ch <- m.Desc
}
for _, m := range TR64Metrics {
ch <- m.Desc
}
}

func (fc *FritzboxCollector) Collect(ch chan<- prometheus.Metric) {
fc.Lock()
root := fc.Root
root := fc.IGDRoot
fc.Unlock()
fc.collectRoot(ch, root, IGDMetrics)

fc.Lock()
root = fc.TR64Root
fc.Unlock()
fc.collectRoot(ch, root, TR64Metrics)
}

func (fc *FritzboxCollector) collectRoot(ch chan<- prometheus.Metric, root *upnp.Root, metrics []*Metric) {
if root == nil {
return // Services not loaded yet
}
Expand Down
27 changes: 19 additions & 8 deletions fritzbox_upnp/action.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,13 @@ import (
"encoding/xml"
"fmt"
"io"
"log"
"net/http"
"strconv"
"strings"

dac "github.com/123Haynes/go-http-digest-auth-client"
"time"
)

// An UPNP Acton on a service
// Action is an UPNP Action on a service
type Action struct {
service *Service

Expand Down Expand Up @@ -78,11 +76,11 @@ func (a *Action) Call() (Result, error) {
req.Header.Set("Content-Type", textXml)
req.Header.Set("SoapAction", action)

t := dac.NewTransport(a.service.Device.root.params.Username, a.service.Device.root.params.Password)
client := a.service.Device.root.client

resp, err := t.RoundTrip(req)
resp, err := client.Transport.RoundTrip(req)
if err != nil {
log.Fatalln(err)
return nil, fmt.Errorf("cannod call %s: %w", a.Name, err)
}
defer closeIgnoringError(resp.Body)

Expand Down Expand Up @@ -157,8 +155,21 @@ func convertResult(val string, arg *Argument) (interface{}, error) {
return nil, err
}
return res, nil
case "i1", "i2", "i4":
res, err := strconv.ParseInt(val, 10, 64)
if err != nil {
return nil, err
}
return res, nil
case "dateTime":
const timeLayout = "2006-01-02T15:04:05"
res, err := time.Parse(timeLayout, val)
if err != nil {
return nil, err
}
return res, nil
default:
return nil, fmt.Errorf("unknown datatype: %s", arg.StateVariable.DataType)
return nil, fmt.Errorf("unknown datatype: %s: %s", arg.StateVariable.DataType, val)

}
}
34 changes: 34 additions & 0 deletions fritzbox_upnp/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package fritzbox_upnp

import (
"crypto/tls"
"net/http"

dac "github.com/ndecker/go-http-digest-auth-client"
)

func setupClient(username string, password string, allowSelfsigned bool) *http.Client {
var t http.RoundTripper
t = &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: allowSelfsigned,
},
}

if username != "" {
client := &http.Client{
Transport: t,
}

t = &dac.DigestTransport{
Client: client,
Username: username,
Password: password,
}
}

client := &http.Client{
Transport: t,
}
return client
}
54 changes: 24 additions & 30 deletions fritzbox_upnp/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,21 +37,25 @@ import (
const textXml = `text/xml; charset="utf-8"`

const (
igdServiceDescriptor = "igddesc.xml"
tr64ServiceDescriptor = "tr64desc.xml"
IGDServiceDescriptor = "igddesc.xml"
TR64ServiceDescriptor = "tr64desc.xml"
)

var ErrInvalidSOAPResponse = errors.New("invalid SOAP response")

type ConnectionParameters struct {
Device string // Hostname or IP
Port int
Username string
Password string
Device string // Hostname or IP
Port int
PortTLS int
UseTLS bool
Username string
Password string
AllowSelfSigned bool
}

// Root of the UPNP tree
type Root struct {
client *http.Client
baseUrl string
params ConnectionParameters
Device Device `xml:"device"`
Expand Down Expand Up @@ -104,9 +108,9 @@ func (d *Device) fillServices(r *Root) error {
for _, s := range d.Services {
s.Device = d

response, err := http.Get(r.baseUrl + s.SCPDUrl)
response, err := r.client.Get(r.baseUrl + s.SCPDUrl)
if err != nil {
return err
return fmt.Errorf("cannot load services for %s: %w", s.SCPDUrl, err)
}

var scpd scpdRoot
Expand Down Expand Up @@ -149,11 +153,20 @@ func (d *Device) fillServices(r *Root) error {
return nil
}

// LoadServiceRoot loads a service descriptor and poplates a Service Root
// LoadServiceRoot loads a service descriptor and populates a Service Root
func LoadServiceRoot(params ConnectionParameters, descriptor string) (*Root, error) {
var baseUrl string
if params.UseTLS {
baseUrl = fmt.Sprintf("https://%s:%d", params.Device, params.PortTLS)
} else {
baseUrl = fmt.Sprintf("http://%s:%d", params.Device, params.Port)

}

var root = &Root{
params: params,
baseUrl: fmt.Sprintf("http://%s:%d", params.Device, params.Port),
client: setupClient(params.Username, params.Password, params.AllowSelfSigned),
baseUrl: baseUrl,
Services: make(map[string]*Service),
}

Expand All @@ -162,7 +175,7 @@ func LoadServiceRoot(params ConnectionParameters, descriptor string) (*Root, err
return nil, err
}

igddesc, err := http.Get(descUrl)
igddesc, err := root.client.Get(descUrl)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -192,25 +205,6 @@ func LoadServiceRoot(params ConnectionParameters, descriptor string) (*Root, err
return root, nil
}

// LoadServices loads the services tree from a device.
func LoadServices(params ConnectionParameters) (*Root, error) {
root, err := LoadServiceRoot(params, igdServiceDescriptor)
if err != nil {
return nil, err // already annotated
}

rootTR64, err := LoadServiceRoot(params, tr64ServiceDescriptor)
if err != nil {
return nil, err // already annotated
}

for k, v := range rootTR64.Services {
root.Services[k] = v
}

return root, nil
}

// closeIgnoringError closes c an ignores errors
func closeIgnoringError(c io.Closer) {
_ = c.Close()
Expand Down
6 changes: 4 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ module github.com/ndecker/fritzbox_exporter

go 1.18

require github.com/prometheus/client_golang v1.14.0
require (
github.com/ndecker/go-http-digest-auth-client v0.4.0
github.com/prometheus/client_golang v1.14.0
)

require (
github.com/123Haynes/go-http-digest-auth-client v0.3.1-0.20171226204513-4c2ff1556cab // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/golang/protobuf v1.5.2 // indirect
Expand Down
Loading

0 comments on commit 62d8380

Please sign in to comment.