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. 77
      fritzbox_upnp/service.go
  2. 40
      main.go

77
fritzbox_upnp/service.go

@ -21,9 +21,12 @@ import (
"errors"
"fmt"
"io"
"log"
"net/http"
"strconv"
"strings"
dac "github.com/123Haynes/go-http-digest-auth-client"
)
// curl http://fritz.box:49000/igddesc.xml
@ -40,7 +43,9 @@ var ErrInvalidSOAPResponse = errors.New("invalid SOAP response")
// Root of the UPNP tree
type Root struct {
BaseUrl string
Device Device `xml:"device"`
Username string
Password string
Device Device `xml:"device"`
Services map[string]*Service // Map of all services indexed by .ServiceType
}
@ -75,7 +80,7 @@ type Service struct {
SCPDUrl string `xml:"SCPDURL"`
Actions map[string]*Action // All actions available on the service
StateVariables []*StateVariable // All state variables available on the service
StateVariables []*StateVariable // All state variables available on the service
}
type scpdRoot struct {
@ -87,20 +92,24 @@ type scpdRoot struct {
type Action struct {
service *Service
Name string `xml:"name"`
Arguments []*Argument `xml:"argumentList>argument"`
Name string `xml:"name"`
Arguments []*Argument `xml:"argumentList>argument"`
ArgumentMap map[string]*Argument // Map of arguments indexed by .Name
}
// 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.
func (a *Action) IsGetOnly() bool {
for _, a := range a.Arguments {
if a.Direction == "in" {
return false
if strings.HasPrefix(a.Name, "Get") {
for _, a := range a.Arguments {
if a.Direction == "in" {
return false
}
}
return len(a.Arguments) > 0
}
return len(a.Arguments) > 0
return false
}
// An Argument to an action
@ -144,6 +153,26 @@ func (r *Root) load() error {
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
func (d *Device) fillServices(r *Root) error {
d.root = r
@ -218,18 +247,19 @@ func (a *Action) Call() (Result, error) {
action := fmt.Sprintf("%s#%s", a.service.ServiceType, a.Name)
req.Header["Content-Type"] = []string{text_xml}
req.Header["SoapAction"] = []string{action}
req.Header.Set("Content-Type", text_xml)
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 {
return nil, err
log.Fatalln(err)
}
data := new(bytes.Buffer)
data.ReadFrom(resp.Body)
// fmt.Printf(data.String())
return a.parseSoapResponse(data)
}
@ -299,9 +329,11 @@ func convertResult(val string, arg *Argument) (interface{}, error) {
}
// 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{
BaseUrl: fmt.Sprintf("http://%s:%d", device, port),
BaseUrl: fmt.Sprintf("http://%s:%d", device, port),
Username: username,
Password: password,
}
err := root.load()
@ -309,5 +341,20 @@ func LoadServices(device string, port uint16) (*Root, error) {
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
}

40
main.go

@ -17,10 +17,10 @@ package main
import (
"flag"
"fmt"
"log"
"net/http"
"sync"
"time"
"log"
"github.com/prometheus/client_golang/prometheus"
@ -33,8 +33,10 @@ var (
flag_test = flag.Bool("test", false, "print all available metrics to stdout")
flag_addr = flag.String("listen-address", ":9133", "The address to listen on for HTTP requests.")
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_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_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 (
@ -165,11 +167,25 @@ var metrics = []*Metric{
),
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 {
Gateway string
Port uint16
Gateway string
Port uint16
Username string
Password string
sync.Mutex // protects Root
Root *upnp.Root
@ -178,7 +194,7 @@ type FritzboxCollector struct {
// LoadServices tries to load the service information. Retries until success.
func (fc *FritzboxCollector) LoadServices() {
for {
root, err := upnp.LoadServices(fc.Gateway, fc.Port)
root, err := upnp.LoadServices(fc.Gateway, fc.Port, fc.Username, fc.Password)
if err != nil {
fmt.Printf("cannot load services: %s\n", err)
@ -280,13 +296,12 @@ func (fc *FritzboxCollector) Collect(ch chan<- prometheus.Metric) {
}
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 {
panic(err)
}
for _, s := range root.Services {
fmt.Printf("%s: %s\n", s.Device.FriendlyName, s.ServiceType)
for _, a := range s.Actions {
if !a.IsGetOnly() {
continue
@ -294,7 +309,8 @@ func test() {
res, err := a.Call()
if err != nil {
panic(err)
fmt.Errorf("unexpected error", err)
continue
}
fmt.Printf(" %s\n", a.Name)
@ -314,8 +330,10 @@ func main() {
}
collector := &FritzboxCollector{
Gateway: *flag_gateway_address,
Port: uint16(*flag_gateway_port),
Gateway: *flag_gateway_address,
Port: uint16(*flag_gateway_port),
Username: *flag_gateway_username,
Password: *flag_gateway_password,
}
go collector.LoadServices()

Loading…
Cancel
Save