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()