From effd5e951567025b6ee8075aa43b25beed9669ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hannes=20Rosen=C3=B6gger?= <123haynes@gmail.com> Date: Tue, 26 Dec 2017 22:55:08 +0100 Subject: [PATCH] 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. --- fritzbox_upnp/service.go | 85 +++++++++++++++++++++++++++++++--------- main.go | 40 +++++++++++++------ 2 files changed, 95 insertions(+), 30 deletions(-) diff --git a/fritzbox_upnp/service.go b/fritzbox_upnp/service.go index f050bb8..9f10e23 100644 --- a/fritzbox_upnp/service.go +++ b/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 @@ -200,10 +229,10 @@ func (d *Device) fillServices(r *Root) error { // Currently only actions without input arguments are supported. func (a *Action) Call() (Result, error) { bodystr := fmt.Sprintf(` - - - - + + + + `, 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) - 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 } diff --git a/main.go b/main.go index 0d3f641..68f859e 100644 --- a/main.go +++ b/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()