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

85
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,7 +43,9 @@ 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
Device Device `xml:"device"` Username string
Password string
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
} }
@ -75,7 +80,7 @@ type Service struct {
SCPDUrl string `xml:"SCPDURL"` SCPDUrl string `xml:"SCPDURL"`
Actions map[string]*Action // All actions available on the service 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 { type scpdRoot struct {
@ -87,20 +92,24 @@ type scpdRoot struct {
type Action struct { type Action struct {
service *Service service *Service
Name string `xml:"name"` Name string `xml:"name"`
Arguments []*Argument `xml:"argumentList>argument"` Arguments []*Argument `xml:"argumentList>argument"`
ArgumentMap map[string]*Argument // Map of arguments indexed by .Name ArgumentMap map[string]*Argument // Map of arguments indexed by .Name
} }
// 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 {
for _, a := range a.Arguments { if strings.HasPrefix(a.Name, "Get") {
if a.Direction == "in" { for _, a := range a.Arguments {
return false if a.Direction == "in" {
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
@ -200,10 +229,10 @@ func (d *Device) fillServices(r *Root) error {
// Currently only actions without input arguments are supported. // Currently only actions without input arguments are supported.
func (a *Action) Call() (Result, error) { func (a *Action) Call() (Result, error) {
bodystr := fmt.Sprintf(` bodystr := fmt.Sprintf(`
<?xml version='1.0' encoding='utf-8'?> <?xml version='1.0' encoding='utf-8'?>
<s:Envelope s:encodingStyle='http://schemas.xmlsoap.org/soap/encoding/' xmlns:s='http://schemas.xmlsoap.org/soap/envelope/'> <s:Envelope s:encodingStyle='http://schemas.xmlsoap.org/soap/encoding/' xmlns:s='http://schemas.xmlsoap.org/soap/envelope/'>
<s:Body> <s:Body>
<u:%s xmlns:u='%s' /> <u:%s xmlns:u='%s' />
</s:Body> </s:Body>
</s:Envelope> </s:Envelope>
`, a.Name, a.service.ServiceType) `, a.Name, a.service.ServiceType)
@ -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
} }

40
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"
@ -33,8 +33,10 @@ var (
flag_test = flag.Bool("test", false, "print all available metrics to stdout") 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_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_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)
@ -314,8 +330,10 @@ 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