Browse Source

first implementation of tr064 support

This implements support for querying the TR064 API of the fritz box.
To make that possible digest authorization has been added.
An example usage of the new api are the total wlan connections for
interface 1.
pull/1/head
Hannes Rosenögger 7 years ago
parent
commit
effd5e9515
  1. 59
      fritzbox_upnp/service.go
  2. 28
      main.go

59
fritzbox_upnp/service.go

@ -21,9 +21,12 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"log"
"net/http" "net/http"
"strconv" "strconv"
"strings" "strings"
dac "github.com/123Haynes/go-http-digest-auth-client"
) )
// curl http://fritz.box:49000/igddesc.xml // curl http://fritz.box:49000/igddesc.xml
@ -40,6 +43,8 @@ var ErrInvalidSOAPResponse = errors.New("invalid SOAP response")
// Root of the UPNP tree // Root of the UPNP tree
type Root struct { type Root struct {
BaseUrl string BaseUrl string
Username string
Password string
Device Device `xml:"device"` Device Device `xml:"device"`
Services map[string]*Service // Map of all services indexed by .ServiceType Services map[string]*Service // Map of all services indexed by .ServiceType
} }
@ -95,12 +100,16 @@ type Action struct {
// Returns if the action seems to be a query for information. // Returns if the action seems to be a query for information.
// This is determined by checking if the action has no input arguments and at least one output argument. // This is determined by checking if the action has no input arguments and at least one output argument.
func (a *Action) IsGetOnly() bool { func (a *Action) IsGetOnly() bool {
if strings.HasPrefix(a.Name, "Get") {
for _, a := range a.Arguments { for _, a := range a.Arguments {
if a.Direction == "in" { if a.Direction == "in" {
return false return false
} }
} }
return len(a.Arguments) > 0 return len(a.Arguments) > 0
}
return false
} }
// An Argument to an action // An Argument to an action
@ -144,6 +153,26 @@ func (r *Root) load() error {
return r.Device.fillServices(r) return r.Device.fillServices(r)
} }
func (r *Root) loadTr64() error {
igddesc, err := http.Get(
fmt.Sprintf("%s/tr64desc.xml", r.BaseUrl),
)
if err != nil {
return err
}
dec := xml.NewDecoder(igddesc.Body)
err = dec.Decode(r)
if err != nil {
return err
}
r.Services = make(map[string]*Service)
return r.Device.fillServices(r)
}
// load all service descriptions // load all service descriptions
func (d *Device) fillServices(r *Root) error { func (d *Device) fillServices(r *Root) error {
d.root = r d.root = r
@ -218,18 +247,19 @@ func (a *Action) Call() (Result, error) {
action := fmt.Sprintf("%s#%s", a.service.ServiceType, a.Name) action := fmt.Sprintf("%s#%s", a.service.ServiceType, a.Name)
req.Header["Content-Type"] = []string{text_xml} req.Header.Set("Content-Type", text_xml)
req.Header["SoapAction"] = []string{action} req.Header.Set("SoapAction", action)
t := dac.NewTransport(a.service.Device.root.Username, a.service.Device.root.Password)
resp, err := http.DefaultClient.Do(req) resp, err := t.RoundTrip(req)
if err != nil { if err != nil {
return nil, err log.Fatalln(err)
} }
data := new(bytes.Buffer) data := new(bytes.Buffer)
data.ReadFrom(resp.Body) data.ReadFrom(resp.Body)
// fmt.Printf(data.String())
return a.parseSoapResponse(data) return a.parseSoapResponse(data)
} }
@ -299,9 +329,11 @@ func convertResult(val string, arg *Argument) (interface{}, error) {
} }
// Load the services tree from an device. // Load the services tree from an device.
func LoadServices(device string, port uint16) (*Root, error) { func LoadServices(device string, port uint16, username string, password string) (*Root, error) {
var root = &Root{ var root = &Root{
BaseUrl: fmt.Sprintf("http://%s:%d", device, port), BaseUrl: fmt.Sprintf("http://%s:%d", device, port),
Username: username,
Password: password,
} }
err := root.load() err := root.load()
@ -309,5 +341,20 @@ func LoadServices(device string, port uint16) (*Root, error) {
return nil, err return nil, err
} }
var rootTr64 = &Root{
BaseUrl: fmt.Sprintf("http://%s:%d", device, port),
Username: username,
Password: password,
}
err = rootTr64.loadTr64()
if err != nil {
return nil, err
}
for k, v := range rootTr64.Services {
root.Services[k] = v
}
return root, nil return root, nil
} }

28
main.go

@ -17,10 +17,10 @@ package main
import ( import (
"flag" "flag"
"fmt" "fmt"
"log"
"net/http" "net/http"
"sync" "sync"
"time" "time"
"log"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
@ -35,6 +35,8 @@ var (
flag_gateway_address = flag.String("gateway-address", "fritz.box", "The hostname or IP of the FRITZ!Box") flag_gateway_address = flag.String("gateway-address", "fritz.box", "The hostname or IP of the FRITZ!Box")
flag_gateway_port = flag.Int("gateway-port", 49000, "The port of the FRITZ!Box UPnP service") flag_gateway_port = flag.Int("gateway-port", 49000, "The port of the FRITZ!Box UPnP service")
flag_gateway_username = flag.String("username", "", "The user for the FRITZ!Box UPnP service")
flag_gateway_password = flag.String("password", "", "The password for the FRITZ!Box UPnP service")
) )
var ( var (
@ -165,11 +167,25 @@ var metrics = []*Metric{
), ),
MetricType: prometheus.GaugeValue, MetricType: prometheus.GaugeValue,
}, },
{
Service: "urn:dslforum-org:service:WLANConfiguration:1",
Action: "GetTotalAssociations",
Result: "TotalAssociations",
Desc: prometheus.NewDesc(
"gateway_wlan_current_connections",
"current WLAN connections",
[]string{"gateway"},
nil,
),
MetricType: prometheus.GaugeValue,
},
} }
type FritzboxCollector struct { type FritzboxCollector struct {
Gateway string Gateway string
Port uint16 Port uint16
Username string
Password string
sync.Mutex // protects Root sync.Mutex // protects Root
Root *upnp.Root Root *upnp.Root
@ -178,7 +194,7 @@ type FritzboxCollector struct {
// LoadServices tries to load the service information. Retries until success. // LoadServices tries to load the service information. Retries until success.
func (fc *FritzboxCollector) LoadServices() { func (fc *FritzboxCollector) LoadServices() {
for { for {
root, err := upnp.LoadServices(fc.Gateway, fc.Port) root, err := upnp.LoadServices(fc.Gateway, fc.Port, fc.Username, fc.Password)
if err != nil { if err != nil {
fmt.Printf("cannot load services: %s\n", err) fmt.Printf("cannot load services: %s\n", err)
@ -280,13 +296,12 @@ func (fc *FritzboxCollector) Collect(ch chan<- prometheus.Metric) {
} }
func test() { func test() {
root, err := upnp.LoadServices(*flag_gateway_address, uint16(*flag_gateway_port)) root, err := upnp.LoadServices(*flag_gateway_address, uint16(*flag_gateway_port), *flag_gateway_username, *flag_gateway_password)
if err != nil { if err != nil {
panic(err) panic(err)
} }
for _, s := range root.Services { for _, s := range root.Services {
fmt.Printf("%s: %s\n", s.Device.FriendlyName, s.ServiceType)
for _, a := range s.Actions { for _, a := range s.Actions {
if !a.IsGetOnly() { if !a.IsGetOnly() {
continue continue
@ -294,7 +309,8 @@ func test() {
res, err := a.Call() res, err := a.Call()
if err != nil { if err != nil {
panic(err) fmt.Errorf("unexpected error", err)
continue
} }
fmt.Printf(" %s\n", a.Name) fmt.Printf(" %s\n", a.Name)
@ -316,6 +332,8 @@ func main() {
collector := &FritzboxCollector{ collector := &FritzboxCollector{
Gateway: *flag_gateway_address, Gateway: *flag_gateway_address,
Port: uint16(*flag_gateway_port), Port: uint16(*flag_gateway_port),
Username: *flag_gateway_username,
Password: *flag_gateway_password,
} }
go collector.LoadServices() go collector.LoadServices()

Loading…
Cancel
Save