diff --git a/fritzbox_upnp/service.go b/fritzbox_upnp/service.go index 1cca2e9..4826e45 100644 --- a/fritzbox_upnp/service.go +++ b/fritzbox_upnp/service.go @@ -16,17 +16,17 @@ package fritzbox_upnp // limitations under the License. import ( + "bytes" + "crypto/md5" + "crypto/rand" + "crypto/tls" "encoding/xml" "errors" - "bytes" "fmt" "io" "net/http" - "crypto/tls" "strconv" "strings" - "crypto/md5" - "crypto/rand" ) // curl http://fritz.box:49000/igddesc.xml @@ -36,7 +36,7 @@ import ( // curl http://fritz.box:49000/igddslSCPD.xml // curl http://fritz.box:49000/igd2ipv6fwcSCPD.xml -const text_xml = `text/xml; charset="utf-8"` +const textXml = `text/xml; charset="utf-8"` var ErrInvalidSOAPResponse = errors.New("invalid SOAP response") @@ -99,34 +99,33 @@ type Action struct { // An InĂ¼ut Argument to pass to an action type ActionArgument struct { - Name string - Value interface{} + Name string + Value interface{} } // structs to unmarshal SOAP faults type SoapEnvelope struct { - XMLName xml.Name `xml:"http://schemas.xmlsoap.org/soap/envelope/ Envelope"` - Body SoapBody + XMLName xml.Name `xml:"http://schemas.xmlsoap.org/soap/envelope/ Envelope"` + Body SoapBody } type SoapBody struct { - XMLName xml.Name `xml:"http://schemas.xmlsoap.org/soap/envelope/ Body"` - Fault SoapFault + XMLName xml.Name `xml:"http://schemas.xmlsoap.org/soap/envelope/ Body"` + Fault SoapFault } type SoapFault struct { - XMLName xml.Name `xml:"http://schemas.xmlsoap.org/soap/envelope/ Fault"` - FaultCode string `xml:"faultcode"` - FaultString string `xml:"faultstring"` - Detail FaultDetail `xml:"detail"` + XMLName xml.Name `xml:"http://schemas.xmlsoap.org/soap/envelope/ Fault"` + FaultCode string `xml:"faultcode"` + FaultString string `xml:"faultstring"` + Detail FaultDetail `xml:"detail"` } type FaultDetail struct { - UpnpError UpnpError `xml:"UPnPError"` + UpnpError UpnpError `xml:"UPnPError"` } type UpnpError struct { - ErrorCode int `xml:"errorCode"` - ErrorDescription string `xml:"errorDescription"` + ErrorCode int `xml:"errorCode"` + ErrorDescription string `xml:"errorDescription"` } - // 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 { @@ -136,9 +135,6 @@ func (a *Action) IsGetOnly() bool { } } return len(a.Arguments) > 0 - - return false - } // An Argument to an action @@ -172,7 +168,7 @@ func (r *Root) load() error { } defer igddesc.Body.Close() - + dec := xml.NewDecoder(igddesc.Body) err = dec.Decode(r) @@ -262,7 +258,7 @@ func (d *Device) fillServices(r *Root) error { const SoapActionXML = `` + `` + - `%s` + + `%s` + `` const SoapActionParamXML = `<%s>%s` @@ -287,18 +283,18 @@ func (a *Action) createCallHttpRequest(actionArg *ActionArgument) (*http.Request action := fmt.Sprintf("%s#%s", a.service.ServiceType, a.Name) - req.Header.Set("Content-Type", text_xml) + req.Header.Set("Content-Type", textXml) req.Header.Set("SOAPAction", action) - return req, nil; -} + return req, nil +} // store auth header for reuse var authHeader = "" // Call an action with argument if given func (a *Action) Call(actionArg *ActionArgument) (Result, error) { - req, err := a.createCallHttpRequest(actionArg) + req, err := a.createCallHttpRequest(actionArg) if err != nil { return nil, err @@ -308,7 +304,7 @@ func (a *Action) Call(actionArg *ActionArgument) (Result, error) { if authHeader != "" { req.Header.Set("Authorization", authHeader) } - + // first try call without auth header resp, err := http.DefaultClient.Do(req) @@ -318,8 +314,8 @@ func (a *Action) Call(actionArg *ActionArgument) (Result, error) { wwwAuth := resp.Header.Get("WWW-Authenticate") if resp.StatusCode == http.StatusUnauthorized { - resp.Body.Close() // close now, since we make a new request below or fail - + resp.Body.Close() // close now, since we make a new request below or fail + if wwwAuth != "" && a.service.Device.root.Username != "" && a.service.Device.root.Password != "" { // call failed, but we have a password so calculate header and try again authHeader, err = a.getDigestAuthHeader(wwwAuth, a.service.Device.root.Username, a.service.Device.root.Password) @@ -327,24 +323,24 @@ func (a *Action) Call(actionArg *ActionArgument) (Result, error) { return nil, errors.New(fmt.Sprintf("%s: %s", a.Name, err.Error)) } - req, err = a.createCallHttpRequest(actionArg) + req, err = a.createCallHttpRequest(actionArg) if err != nil { return nil, errors.New(fmt.Sprintf("%s: %s", a.Name, err.Error)) } req.Header.Set("Authorization", authHeader) - - resp, err = http.DefaultClient.Do(req) + + resp, err = http.DefaultClient.Do(req) if err != nil { return nil, errors.New(fmt.Sprintf("%s: %s", a.Name, err.Error)) } - + } else { return nil, errors.New(fmt.Sprintf("%s: Unauthorized, but no username and password given", a.Name)) } } - + defer resp.Body.Close() if resp.StatusCode != http.StatusOK { @@ -354,22 +350,22 @@ func (a *Action) Call(actionArg *ActionArgument) (Result, error) { io.Copy(buf, resp.Body) body := buf.String() //fmt.Println(body) - + var soapEnv SoapEnvelope err := xml.Unmarshal([]byte(body), &soapEnv) if err != nil { errMsg = fmt.Sprintf("error decoding SOAPFault: %s", err.Error()) } else { soapFault := soapEnv.Body.Fault - + if soapFault.FaultString == "UPnPError" { - upe := soapFault.Detail.UpnpError; - + upe := soapFault.Detail.UpnpError + errMsg = fmt.Sprintf("SAOPFault: %s %d (%s)", soapFault.FaultString, upe.ErrorCode, upe.ErrorDescription) } else { errMsg = fmt.Sprintf("SAOPFault: %s", soapFault.FaultString) } - } + } } return nil, errors.New(fmt.Sprintf("%s: %s", a.Name, errMsg)) } @@ -379,10 +375,10 @@ func (a *Action) Call(actionArg *ActionArgument) (Result, error) { func (a *Action) getDigestAuthHeader(wwwAuth string, username string, password string) (string, error) { // parse www-auth header - if ! strings.HasPrefix(wwwAuth, "Digest ") { - return "", errors.New(fmt.Sprintf("WWW-Authentication header is not Digest: '%s'", wwwAuth)) + if !strings.HasPrefix(wwwAuth, "Digest ") { + return "", errors.New(fmt.Sprintf("WWW-Authentication header is not Digest: '%s'", wwwAuth)) } - + s := wwwAuth[7:] d := map[string]string{} for _, kv := range strings.Split(s, ",") { @@ -392,39 +388,38 @@ func (a *Action) getDigestAuthHeader(wwwAuth string, username string, password s } d[strings.Trim(parts[0], "\" ")] = strings.Trim(parts[1], "\" ") } - + if d["algorithm"] == "" { d["algorithm"] = "MD5" } else if d["algorithm"] != "MD5" { return "", errors.New(fmt.Sprintf("digest algorithm not supported: %s != MD5", d["algorithm"])) } - + if d["qop"] != "auth" { return "", errors.New(fmt.Sprintf("digest qop not supported: %s != auth", d["qop"])) } // calc h1 and h2 - ha1 := fmt.Sprintf("%x", md5.Sum([]byte(username + ":" + d["realm"] + ":" + password))) - - ha2 := fmt.Sprintf("%x", md5.Sum([]byte("POST:" + a.service.ControlUrl))) + ha1 := fmt.Sprintf("%x", md5.Sum([]byte(username+":"+d["realm"]+":"+password))) + + ha2 := fmt.Sprintf("%x", md5.Sum([]byte("POST:"+a.service.ControlUrl))) cn := make([]byte, 8) - rand.Read(cn) - cnonce := fmt.Sprintf("%x", cn) - - nCounter := 1 - nc:=fmt.Sprintf("%08x", nCounter) + rand.Read(cn) + cnonce := fmt.Sprintf("%x", cn) + + nCounter := 1 + nc := fmt.Sprintf("%08x", nCounter) ds := strings.Join([]string{ha1, d["nonce"], nc, cnonce, d["qop"], ha2}, ":") response := fmt.Sprintf("%x", md5.Sum([]byte(ds))) - + authHeader := fmt.Sprintf("Digest username=\"%s\", realm=\"%s\", nonce=\"%s\", uri=\"%s\", cnonce=\"%s\", nc=%s, qop=%s, response=\"%s\", algorithm=%s", - username, d["realm"], d["nonce"], a.service.ControlUrl, cnonce, nc, d["qop"], response, d["algorithm"]) - + username, d["realm"], d["nonce"], a.service.ControlUrl, cnonce, nc, d["qop"], response, d["algorithm"]) + return authHeader, nil } - func (a *Action) parseSoapResponse(r io.Reader) (Result, error) { res := make(Result) dec := xml.NewDecoder(r) @@ -482,7 +477,7 @@ func convertResult(val string, arg *Argument) (interface{}, error) { if err != nil { return nil, err } - return uint64(res), nil + return res, nil case "i4": res, err := strconv.ParseInt(val, 10, 64) if err != nil { @@ -491,7 +486,7 @@ func convertResult(val string, arg *Argument) (interface{}, error) { return int64(res), nil case "dateTime", "uuid": // data types we don't convert yet - return val, nil + return val, nil default: return nil, fmt.Errorf("unknown datatype: %s (%s)", arg.StateVariable.DataType, val) } diff --git a/main.go b/main.go index 05e0058..7614dc0 100644 --- a/main.go +++ b/main.go @@ -39,26 +39,26 @@ import ( const serviceLoadRetryTime = 1 * time.Minute var ( - flag_test = flag.Bool("test", false, "print all available metrics to stdout") - flag_collect = flag.Bool("collect", false, "print configured metrics to stdout and exit") - flag_jsonout = flag.String("json-out", "", "store metrics also to JSON file when running test") + flagTest = flag.Bool("test", false, "print all available metrics to stdout") + flagCollect = flag.Bool("collect", false, "print configured metrics to stdout and exit") + flagJsonOut = flag.String("json-out", "", "store metrics also to JSON file when running test") - flag_addr = flag.String("listen-address", "127.0.0.1:9042", "The address to listen on for HTTP requests.") - flag_metrics_file = flag.String("metrics-file", "metrics.json", "The JSON file with the metric definitions.") + flagAddr = flag.String("listen-address", "127.0.0.1:9042", "The address to listen on for HTTP requests.") + flagMetricsFile = flag.String("metrics-file", "metrics.json", "The JSON file with the metric definitions.") - flag_gateway_url = flag.String("gateway-url", "http://fritz.box:49000", "The URL of the FRITZ!Box") - 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") + flagGatewayUrl = flag.String("gateway-url", "http://fritz.box:49000", "The URL of the FRITZ!Box") + flagGatewayUsername = flag.String("username", "", "The user for the FRITZ!Box UPnP service") + flagGatewayPassword = flag.String("password", "", "The password for the FRITZ!Box UPnP service") ) var ( - collect_errors = prometheus.NewCounter(prometheus.CounterOpts{ + collectErrors = prometheus.NewCounter(prometheus.CounterOpts{ Name: "fritzbox_exporter_collect_errors", Help: "Number of collection errors.", }) ) -type JSON_PromDesc struct { +type JsonPromDesc struct { FqName string `json:"fqName"` Help string `json:"help"` VarLabels []string `json:"varLabels"` @@ -73,13 +73,13 @@ type ActionArg struct { type Metric struct { // initialized loading JSON - Service string `json:"service"` - Action string `json:"action"` - ActionArgument *ActionArg `json:"actionArgument"` - Result string `json:"result"` - OkValue string `json:"okValue"` - PromDesc JSON_PromDesc `json:"promDesc"` - PromType string `json:"promType"` + Service string `json:"service"` + Action string `json:"action"` + ActionArgument *ActionArg `json:"actionArgument"` + Result string `json:"result"` + OkValue string `json:"okValue"` + PromDesc JsonPromDesc `json:"promDesc"` + PromType string `json:"promType"` // initialized at startup Desc *prometheus.Desc @@ -152,7 +152,7 @@ func (fc *FritzboxCollector) ReportMetric(ch chan<- prometheus.Metric, m *Metric val, ok := result[m.Result] if !ok { fmt.Printf("%s.%s has no result %s", m.Service, m.Action, m.Result) - collect_errors.Inc() + collectErrors.Inc() return } @@ -174,7 +174,7 @@ func (fc *FritzboxCollector) ReportMetric(ch chan<- prometheus.Metric, m *Metric } default: fmt.Println("unknown type", val) - collect_errors.Inc() + collectErrors.Inc() return } @@ -189,7 +189,7 @@ func (fc *FritzboxCollector) ReportMetric(ch chan<- prometheus.Metric, m *Metric lval = "" } - // convert tolower to avoid problems with labels like hostname + // convert to lower to avoid problems with labels like hostname labels[i] = strings.ToLower(fmt.Sprintf("%v", lval)) } } @@ -201,18 +201,18 @@ func (fc *FritzboxCollector) ReportMetric(ch chan<- prometheus.Metric, m *Metric labels...) } -func (fc *FritzboxCollector) GetActionResult(result_map map[string]upnp.Result, serviceType string, actionName string, actionArg *upnp.ActionArgument) (upnp.Result, error) { +func (fc *FritzboxCollector) GetActionResult(resultMap map[string]upnp.Result, serviceType string, actionName string, actionArg *upnp.ActionArgument) (upnp.Result, error) { - m_key := serviceType + "|" + actionName + mKey := serviceType + "|" + actionName // for calls with argument also add arguement name and value to key if actionArg != nil { - m_key += "|" + actionArg.Name + "|" + fmt.Sprintf("%v", actionArg.Value) + mKey += "|" + actionArg.Name + "|" + fmt.Sprintf("%v", actionArg.Value) } - last_result := result_map[m_key] - if last_result == nil { + lastResult := resultMap[mKey] + if lastResult == nil { service, ok := fc.Root.Services[serviceType] if !ok { return nil, errors.New(fmt.Sprintf("service %s not found", serviceType)) @@ -224,16 +224,16 @@ func (fc *FritzboxCollector) GetActionResult(result_map map[string]upnp.Result, } var err error - last_result, err = action.Call(actionArg) + lastResult, err = action.Call(actionArg) if err != nil { return nil, err } - result_map[m_key] = last_result + resultMap[mKey] = lastResult } - return last_result, nil + return lastResult, nil } func (fc *FritzboxCollector) Collect(ch chan<- prometheus.Metric) { @@ -247,7 +247,7 @@ func (fc *FritzboxCollector) Collect(ch chan<- prometheus.Metric) { } // create a map for caching results - var result_map = make(map[string]upnp.Result) + var resultMap = make(map[string]upnp.Result) for _, m := range metrics { var actArg *upnp.ActionArgument @@ -257,19 +257,19 @@ func (fc *FritzboxCollector) Collect(ch chan<- prometheus.Metric) { value = aa.Value if aa.ProviderAction != "" { - provRes, err := fc.GetActionResult(result_map, m.Service, aa.ProviderAction, nil) + provRes, err := fc.GetActionResult(resultMap, m.Service, aa.ProviderAction, nil) if err != nil { fmt.Printf("Error getting provider action %s result for %s.%s: %s\n", aa.ProviderAction, m.Service, m.Action, err.Error()) - collect_errors.Inc() + collectErrors.Inc() continue } var ok bool value, ok = provRes[aa.Value] // Value contains the result name for provider actions if !ok { - fmt.Printf("provider action %s for %s.%s has no result %s", m.Service, m.Action, aa.Value) - collect_errors.Inc() + fmt.Printf("provider action %s for %s.%s has no result", m.Service, m.Action, aa.Value) + collectErrors.Inc() continue } } @@ -279,17 +279,17 @@ func (fc *FritzboxCollector) Collect(ch chan<- prometheus.Metric) { count, err := strconv.Atoi(sval) if err != nil { fmt.Println(err.Error()) - collect_errors.Inc() + collectErrors.Inc() continue } for i := 0; i < count; i++ { actArg = &upnp.ActionArgument{Name: aa.Name, Value: i} - result, err := fc.GetActionResult(result_map, m.Service, m.Action, actArg) + result, err := fc.GetActionResult(resultMap, m.Service, m.Action, actArg) if err != nil { fmt.Println(err.Error()) - collect_errors.Inc() + collectErrors.Inc() continue } @@ -302,11 +302,11 @@ func (fc *FritzboxCollector) Collect(ch chan<- prometheus.Metric) { } } - result, err := fc.GetActionResult(result_map, m.Service, m.Action, actArg) + result, err := fc.GetActionResult(resultMap, m.Service, m.Action, actArg) if err != nil { fmt.Println(err.Error()) - collect_errors.Inc() + collectErrors.Inc() continue } @@ -315,7 +315,7 @@ func (fc *FritzboxCollector) Collect(ch chan<- prometheus.Metric) { } func test() { - root, err := upnp.LoadServices(*flag_gateway_url, *flag_gateway_username, *flag_gateway_password) + root, err := upnp.LoadServices(*flagGatewayUrl, *flagGatewayUsername, *flagGatewayPassword) if err != nil { panic(err) } @@ -333,7 +333,7 @@ func test() { s := root.Services[k] fmt.Printf("Service: %s (Url: %s)\n", k, s.ControlUrl) - actionKeys := []string{} + var actionKeys []string for l, _ := range s.Actions { actionKeys = append(actionKeys, l) } @@ -386,10 +386,10 @@ func test() { json.WriteString("\n]") - if *flag_jsonout != "" { - err := ioutil.WriteFile(*flag_jsonout, json.Bytes(), 0644) + if *flagJsonOut != "" { + err := ioutil.WriteFile(*flagJsonOut, json.Bytes(), 0644) if err != nil { - fmt.Printf("Failed writing JSON file '%s': %s\n", *flag_jsonout, err.Error()) + fmt.Printf("Failed writing JSON file '%s': %s\n", *flagJsonOut, err.Error()) } } } @@ -410,19 +410,19 @@ func getValueType(vt string) prometheus.ValueType { func main() { flag.Parse() - u, err := url.Parse(*flag_gateway_url) + u, err := url.Parse(*flagGatewayUrl) if err != nil { fmt.Println("invalid URL:", err) return } - if *flag_test { + if *flagTest { test() return } // read metrics - jsonData, err := ioutil.ReadFile(*flag_metrics_file) + jsonData, err := ioutil.ReadFile(*flagMetricsFile) if err != nil { fmt.Println("error reading metric file:", err) return @@ -449,17 +449,17 @@ func main() { } collector := &FritzboxCollector{ - Url: *flag_gateway_url, + Url: *flagGatewayUrl, Gateway: u.Hostname(), - Username: *flag_gateway_username, - Password: *flag_gateway_password, + Username: *flagGatewayUsername, + Password: *flagGatewayPassword, } - if *flag_collect { + if *flagCollect { collector.LoadServices() prometheus.MustRegister(collector) - prometheus.MustRegister(collect_errors) + prometheus.MustRegister(collectErrors) fmt.Println("collecting metrics via http") @@ -476,10 +476,10 @@ func main() { go collector.LoadServices() prometheus.MustRegister(collector) - prometheus.MustRegister(collect_errors) + prometheus.MustRegister(collectErrors) http.Handle("/metrics", promhttp.Handler()) - fmt.Printf("metrics available at http://%s/metrics\n", *flag_addr) + fmt.Printf("metrics available at http://%s/metrics\n", *flagAddr) - log.Fatal(http.ListenAndServe(*flag_addr, nil)) + log.Fatal(http.ListenAndServe(*flagAddr, nil)) }