Browse Source

Fixes all names to be compliant with the golang standards.

pull/1/head
Christian Fritz 5 years ago
parent
commit
6713abd7d5
No known key found for this signature in database GPG Key ID: AB40486FCA9FA29C
  1. 115
      fritzbox_upnp/service.go
  2. 108
      main.go

115
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 = `<?xml version="1.0" encoding="utf-8"?>` +
`<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">` +
`<s:Body><u:%s xmlns:u=%s>%s</u:%s xmlns:u=%s></s:Body>` +
`<s:Body><u:%s xmlns:u=%s>%s</u:%s xmlns:u=%s></s:Body>` +
`</s:Envelope>`
const SoapActionParamXML = `<%s>%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)
}

108
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))
}

Loading…
Cancel
Save