Browse Source

cleaned up golint issues

pull/4/head
sberk42 4 years ago
parent
commit
a8da0da336
  1. 1
      fritzbox_lua/README.md
  2. 155
      fritzbox_upnp/service.go
  3. 199
      main.go

1
fritzbox_lua/README.md

@ -7,6 +7,7 @@ There does not seem to be a complete documentation of the API, the authenticatio
## Details ## Details
Most of the calls seem to be using the data.lua url with a http FORM POST request. As parameters the page and session id are required (e.g.: sid=<SID>&page=engery). The result is JSON with the data needed to create the respective UI. Most of the calls seem to be using the data.lua url with a http FORM POST request. As parameters the page and session id are required (e.g.: sid=<SID>&page=engery). The result is JSON with the data needed to create the respective UI.
Some calls (like inetstat_monitor.lua) seem to use GET rather than POST, the client also supports them, but prefix GET: is needed, otherwise a post is done.
Since no public documentation for the JSON format of the various pages seem to exist, you need to observe the calls made by the UI and analyse the JSON result. However the client should be generic enough to get metric and label values from all kind of nested hash and array structures contained in the JSONs. Since no public documentation for the JSON format of the various pages seem to exist, you need to observe the calls made by the UI and analyse the JSON result. However the client should be generic enough to get metric and label values from all kind of nested hash and array structures contained in the JSONs.

155
fritzbox_upnp/service.go

@ -1,4 +1,4 @@
// Query UPNP variables from Fritz!Box devices. // Package fritzbox_upnp Query UPNP variables from Fritz!Box devices.
package fritzbox_upnp package fritzbox_upnp
// Copyright 2016 Nils Decker // Copyright 2016 Nils Decker
@ -16,17 +16,17 @@ package fritzbox_upnp
// limitations under the License. // limitations under the License.
import ( import (
"bytes"
"crypto/md5"
"crypto/rand"
"crypto/tls"
"encoding/xml" "encoding/xml"
"errors" "errors"
"bytes"
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
"crypto/tls"
"strconv" "strconv"
"strings" "strings"
"crypto/md5"
"crypto/rand"
) )
// curl http://fritz.box:49000/igddesc.xml // curl http://fritz.box:49000/igddesc.xml
@ -36,47 +36,47 @@ import (
// curl http://fritz.box:49000/igddslSCPD.xml // curl http://fritz.box:49000/igddslSCPD.xml
// curl http://fritz.box:49000/igd2ipv6fwcSCPD.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") 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
Username string Username string
Password string Password string
Device Device `xml:"device"` 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
} }
// An UPNP Device // Device an UPNP device
type Device struct { type Device struct {
root *Root root *Root
DeviceType string `xml:"deviceType"` DeviceType string `xml:"deviceType"`
FriendlyName string `xml:"friendlyName"` FriendlyName string `xml:"friendlyName"`
Manufacturer string `xml:"manufacturer"` Manufacturer string `xml:"manufacturer"`
ManufacturerUrl string `xml:"manufacturerURL"` ManufacturerURL string `xml:"manufacturerURL"`
ModelDescription string `xml:"modelDescription"` ModelDescription string `xml:"modelDescription"`
ModelName string `xml:"modelName"` ModelName string `xml:"modelName"`
ModelNumber string `xml:"modelNumber"` ModelNumber string `xml:"modelNumber"`
ModelUrl string `xml:"modelURL"` ModelURL string `xml:"modelURL"`
UDN string `xml:"UDN"` UDN string `xml:"UDN"`
Services []*Service `xml:"serviceList>service"` // Service of the device Services []*Service `xml:"serviceList>service"` // Service of the device
Devices []*Device `xml:"deviceList>device"` // Sub-Devices of the device Devices []*Device `xml:"deviceList>device"` // Sub-Devices of the device
PresentationUrl string `xml:"presentationURL"` PresentationURL string `xml:"presentationURL"`
} }
// An UPNP Service // Service an UPNP Service
type Service struct { type Service struct {
Device *Device Device *Device
ServiceType string `xml:"serviceType"` ServiceType string `xml:"serviceType"`
ServiceId string `xml:"serviceId"` ServiceID string `xml:"serviceId"`
ControlUrl string `xml:"controlURL"` ControlURL string `xml:"controlURL"`
EventSubUrl string `xml:"eventSubURL"` EventSubURL string `xml:"eventSubURL"`
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
@ -88,7 +88,7 @@ type scpdRoot struct {
StateVariables []*StateVariable `xml:"serviceStateTable>stateVariable"` StateVariables []*StateVariable `xml:"serviceStateTable>stateVariable"`
} }
// An UPNP Acton on a service // Action an UPNP action on a service
type Action struct { type Action struct {
service *Service service *Service
@ -97,37 +97,44 @@ type Action struct {
ArgumentMap map[string]*Argument // Map of arguments indexed by .Name ArgumentMap map[string]*Argument // Map of arguments indexed by .Name
} }
// An Inüut Argument to pass to an action // ActionArgument an Inüut Argument to pass to an action
type ActionArgument struct { type ActionArgument struct {
Name string Name string
Value interface{} Value interface{}
} }
// structs to unmarshal SOAP faults // SoapEnvelope struct to unmarshal SOAP faults
type SoapEnvelope struct { type SoapEnvelope struct {
XMLName xml.Name `xml:"http://schemas.xmlsoap.org/soap/envelope/ Envelope"` XMLName xml.Name `xml:"http://schemas.xmlsoap.org/soap/envelope/ Envelope"`
Body SoapBody Body SoapBody
} }
// SoapBody struct
type SoapBody struct { type SoapBody struct {
XMLName xml.Name `xml:"http://schemas.xmlsoap.org/soap/envelope/ Body"` XMLName xml.Name `xml:"http://schemas.xmlsoap.org/soap/envelope/ Body"`
Fault SoapFault Fault SoapFault
} }
// SoapFault struct
type SoapFault struct { type SoapFault struct {
XMLName xml.Name `xml:"http://schemas.xmlsoap.org/soap/envelope/ Fault"` XMLName xml.Name `xml:"http://schemas.xmlsoap.org/soap/envelope/ Fault"`
FaultCode string `xml:"faultcode"` FaultCode string `xml:"faultcode"`
FaultString string `xml:"faultstring"` FaultString string `xml:"faultstring"`
Detail FaultDetail `xml:"detail"` Detail FaultDetail `xml:"detail"`
} }
// FaultDetail struct
type FaultDetail struct { type FaultDetail struct {
UpnpError UpnpError `xml:"UPnPError"` UpnpError UpnpError `xml:"UPnPError"`
} }
// UpnpError struct
type UpnpError struct { type UpnpError struct {
ErrorCode int `xml:"errorCode"` ErrorCode int `xml:"errorCode"`
ErrorDescription string `xml:"errorDescription"` ErrorDescription string `xml:"errorDescription"`
} }
// IsGetOnly 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 { for _, a := range a.Arguments {
@ -136,9 +143,6 @@ func (a *Action) IsGetOnly() bool {
} }
} }
return len(a.Arguments) > 0 return len(a.Arguments) > 0
return false
} }
// An Argument to an action // An Argument to an action
@ -149,14 +153,14 @@ type Argument struct {
StateVariable *StateVariable StateVariable *StateVariable
} }
// A state variable that can be manipulated through actions // StateVariable a state variable that can be manipulated through actions
type StateVariable struct { type StateVariable struct {
Name string `xml:"name"` Name string `xml:"name"`
DataType string `xml:"dataType"` DataType string `xml:"dataType"`
DefaultValue string `xml:"defaultValue"` DefaultValue string `xml:"defaultValue"`
} }
// The result of a Call() contains all output arguments of the call. // Result The result of a Call() contains all output arguments of the call.
// The map is indexed by the name of the state variable. // The map is indexed by the name of the state variable.
// The type of the value is string, uint64 or bool depending of the DataType of the variable. // The type of the value is string, uint64 or bool depending of the DataType of the variable.
type Result map[string]interface{} type Result map[string]interface{}
@ -164,7 +168,7 @@ type Result map[string]interface{}
// load the whole tree // load the whole tree
func (r *Root) load() error { func (r *Root) load() error {
igddesc, err := http.Get( igddesc, err := http.Get(
fmt.Sprintf("%s/igddesc.xml", r.BaseUrl), fmt.Sprintf("%s/igddesc.xml", r.BaseURL),
) )
if err != nil { if err != nil {
@ -186,7 +190,7 @@ func (r *Root) load() error {
func (r *Root) loadTr64() error { func (r *Root) loadTr64() error {
igddesc, err := http.Get( igddesc, err := http.Get(
fmt.Sprintf("%s/tr64desc.xml", r.BaseUrl), fmt.Sprintf("%s/tr64desc.xml", r.BaseURL),
) )
if err != nil { if err != nil {
@ -213,7 +217,7 @@ func (d *Device) fillServices(r *Root) error {
for _, s := range d.Services { for _, s := range d.Services {
s.Device = d s.Device = d
response, err := http.Get(r.BaseUrl + s.SCPDUrl) response, err := http.Get(r.BaseURL + s.SCPDUrl)
if err != nil { if err != nil {
return err return err
} }
@ -260,24 +264,24 @@ func (d *Device) fillServices(r *Root) error {
return nil return nil
} }
const SoapActionXML = `<?xml version="1.0" encoding="utf-8"?>` + 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: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>` `</s:Envelope>`
const SoapActionParamXML = `<%s>%s</%s>` const soapActionParamXML = `<%s>%s</%s>`
func (a *Action) createCallHttpRequest(actionArg *ActionArgument) (*http.Request, error) { func (a *Action) createCallHTTPRequest(actionArg *ActionArgument) (*http.Request, error) {
argsString := "" argsString := ""
if actionArg != nil { if actionArg != nil {
var buf bytes.Buffer var buf bytes.Buffer
sValue := fmt.Sprintf("%v", actionArg.Value) sValue := fmt.Sprintf("%v", actionArg.Value)
xml.EscapeText(&buf, []byte(sValue)) xml.EscapeText(&buf, []byte(sValue))
argsString += fmt.Sprintf(SoapActionParamXML, actionArg.Name, buf.String(), actionArg.Name) argsString += fmt.Sprintf(soapActionParamXML, actionArg.Name, buf.String(), actionArg.Name)
} }
bodystr := fmt.Sprintf(SoapActionXML, a.Name, a.service.ServiceType, argsString, a.Name, a.service.ServiceType) bodystr := fmt.Sprintf(soapActionXML, a.Name, a.service.ServiceType, argsString, a.Name, a.service.ServiceType)
url := a.service.Device.root.BaseUrl + a.service.ControlUrl url := a.service.Device.root.BaseURL + a.service.ControlURL
body := strings.NewReader(bodystr) body := strings.NewReader(bodystr)
req, err := http.NewRequest("POST", url, body) req, err := http.NewRequest("POST", url, body)
@ -287,10 +291,10 @@ func (a *Action) createCallHttpRequest(actionArg *ActionArgument) (*http.Request
action := fmt.Sprintf("%s#%s", a.service.ServiceType, a.Name) 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) req.Header.Set("SOAPAction", action)
return req, nil; return req, nil
} }
// store auth header for reuse // store auth header for reuse
@ -298,7 +302,7 @@ var authHeader = ""
// Call an action with argument if given // Call an action with argument if given
func (a *Action) Call(actionArg *ActionArgument) (Result, error) { func (a *Action) Call(actionArg *ActionArgument) (Result, error) {
req, err := a.createCallHttpRequest(actionArg) req, err := a.createCallHTTPRequest(actionArg)
if err != nil { if err != nil {
return nil, err return nil, err
@ -318,18 +322,18 @@ func (a *Action) Call(actionArg *ActionArgument) (Result, error) {
wwwAuth := resp.Header.Get("WWW-Authenticate") wwwAuth := resp.Header.Get("WWW-Authenticate")
if resp.StatusCode == http.StatusUnauthorized { 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 != "" { 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 // 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) authHeader, err = a.getDigestAuthHeader(wwwAuth, a.service.Device.root.Username, a.service.Device.root.Password)
if err != nil { if err != nil {
return nil, errors.New(fmt.Sprintf("%s: %s", a.Name, err.Error)) return nil, fmt.Errorf("%s: %s", a.Name, err.Error())
} }
req, err = a.createCallHttpRequest(actionArg) req, err = a.createCallHTTPRequest(actionArg)
if err != nil { if err != nil {
return nil, errors.New(fmt.Sprintf("%s: %s", a.Name, err.Error)) return nil, fmt.Errorf("%s: %s", a.Name, err.Error())
} }
req.Header.Set("Authorization", authHeader) req.Header.Set("Authorization", authHeader)
@ -337,11 +341,11 @@ func (a *Action) Call(actionArg *ActionArgument) (Result, error) {
resp, err = http.DefaultClient.Do(req) resp, err = http.DefaultClient.Do(req)
if err != nil { if err != nil {
return nil, errors.New(fmt.Sprintf("%s: %s", a.Name, err.Error)) return nil, fmt.Errorf("%s: %s", a.Name, err.Error())
} }
} else { } else {
return nil, errors.New(fmt.Sprintf("%s: Unauthorized, but no username and password given", a.Name)) return nil, fmt.Errorf("%s: Unauthorized, but no username and password given", a.Name)
} }
} }
@ -363,7 +367,7 @@ func (a *Action) Call(actionArg *ActionArgument) (Result, error) {
soapFault := soapEnv.Body.Fault soapFault := soapEnv.Body.Fault
if soapFault.FaultString == "UPnPError" { 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) errMsg = fmt.Sprintf("SAOPFault: %s %d (%s)", soapFault.FaultString, upe.ErrorCode, upe.ErrorDescription)
} else { } else {
@ -371,7 +375,7 @@ func (a *Action) Call(actionArg *ActionArgument) (Result, error) {
} }
} }
} }
return nil, errors.New(fmt.Sprintf("%s: %s", a.Name, errMsg)) return nil, fmt.Errorf("%s: %s", a.Name, errMsg)
} }
return a.parseSoapResponse(resp.Body) return a.parseSoapResponse(resp.Body)
@ -379,8 +383,8 @@ func (a *Action) Call(actionArg *ActionArgument) (Result, error) {
func (a *Action) getDigestAuthHeader(wwwAuth string, username string, password string) (string, error) { func (a *Action) getDigestAuthHeader(wwwAuth string, username string, password string) (string, error) {
// parse www-auth header // parse www-auth header
if ! strings.HasPrefix(wwwAuth, "Digest ") { if !strings.HasPrefix(wwwAuth, "Digest ") {
return "", errors.New(fmt.Sprintf("WWW-Authentication header is not Digest: '%s'", wwwAuth)) return "", fmt.Errorf("WWW-Authentication header is not Digest: '%s'", wwwAuth)
} }
s := wwwAuth[7:] s := wwwAuth[7:]
@ -396,35 +400,34 @@ func (a *Action) getDigestAuthHeader(wwwAuth string, username string, password s
if d["algorithm"] == "" { if d["algorithm"] == "" {
d["algorithm"] = "MD5" d["algorithm"] = "MD5"
} else if d["algorithm"] != "MD5" { } else if d["algorithm"] != "MD5" {
return "", errors.New(fmt.Sprintf("digest algorithm not supported: %s != MD5", d["algorithm"])) return "", fmt.Errorf("digest algorithm not supported: %s != MD5", d["algorithm"])
} }
if d["qop"] != "auth" { if d["qop"] != "auth" {
return "", errors.New(fmt.Sprintf("digest qop not supported: %s != auth", d["qop"])) return "", fmt.Errorf("digest qop not supported: %s != auth", d["qop"])
} }
// calc h1 and h2 // calc h1 and h2
ha1 := fmt.Sprintf("%x", md5.Sum([]byte(username + ":" + d["realm"] + ":" + password))) ha1 := fmt.Sprintf("%x", md5.Sum([]byte(username+":"+d["realm"]+":"+password)))
ha2 := fmt.Sprintf("%x", md5.Sum([]byte("POST:" + a.service.ControlUrl))) ha2 := fmt.Sprintf("%x", md5.Sum([]byte("POST:"+a.service.ControlURL)))
cn := make([]byte, 8) cn := make([]byte, 8)
rand.Read(cn) rand.Read(cn)
cnonce := fmt.Sprintf("%x", cn) cnonce := fmt.Sprintf("%x", cn)
nCounter := 1 nCounter := 1
nc:=fmt.Sprintf("%08x", nCounter) nc := fmt.Sprintf("%08x", nCounter)
ds := strings.Join([]string{ha1, d["nonce"], nc, cnonce, d["qop"], ha2}, ":") ds := strings.Join([]string{ha1, d["nonce"], nc, cnonce, d["qop"], ha2}, ":")
response := fmt.Sprintf("%x", md5.Sum([]byte(ds))) 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", 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 return authHeader, nil
} }
func (a *Action) parseSoapResponse(r io.Reader) (Result, error) { func (a *Action) parseSoapResponse(r io.Reader) (Result, error) {
res := make(Result) res := make(Result)
dec := xml.NewDecoder(r) dec := xml.NewDecoder(r)
@ -455,7 +458,7 @@ func (a *Action) parseSoapResponse(r io.Reader) (Result, error) {
case xml.CharData: case xml.CharData:
val = string(element) val = string(element)
default: default:
return nil, ErrInvalidSOAPResponse return nil, errInvalidSOAPResponse
} }
converted, err := convertResult(val, arg) converted, err := convertResult(val, arg)
@ -497,7 +500,7 @@ func convertResult(val string, arg *Argument) (interface{}, error) {
} }
} }
// Load the services tree from an device. // LoadServices load the services tree from an device.
func LoadServices(baseurl string, username string, password string) (*Root, error) { func LoadServices(baseurl string, username string, password string) (*Root, error) {
if strings.HasPrefix(baseurl, "https://") { if strings.HasPrefix(baseurl, "https://") {
@ -506,7 +509,7 @@ func LoadServices(baseurl string, username string, password string) (*Root, erro
} }
var root = &Root{ var root = &Root{
BaseUrl: baseurl, BaseURL: baseurl,
Username: username, Username: username,
Password: password, Password: password,
} }
@ -517,7 +520,7 @@ func LoadServices(baseurl string, username string, password string) (*Root, erro
} }
var rootTr64 = &Root{ var rootTr64 = &Root{
BaseUrl: baseurl, BaseURL: baseurl,
Username: username, Username: username,
Password: password, Password: password,
} }

199
main.go

@ -43,31 +43,31 @@ const serviceLoadRetryTime = 1 * time.Minute
const minCacheTTL = 30 const minCacheTTL = 30
var ( var (
flag_test = flag.Bool("test", false, "print all available metrics to stdout") flagTest = flag.Bool("test", false, "print all available metrics to stdout")
flag_luatest = flag.Bool("testLua", false, "read luaTest.json file make all contained calls and dump results") flagLuaTest = flag.Bool("testLua", false, "read luaTest.json file make all contained calls and dump results")
flag_collect = flag.Bool("collect", false, "print configured metrics to stdout and exit") flagCollect = 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") 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.") flagAddr = 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.") flagMetricsFile = flag.String("metrics-file", "metrics.json", "The JSON file with the metric definitions.")
flag_disable_lua = flag.Bool("nolua", false, "disable collecting lua metrics") flagDisableLua = flag.Bool("nolua", false, "disable collecting lua metrics")
flag_lua_metrics_file = flag.String("lua-metrics-file", "metrics-lua.json", "The JSON file with the lua metric definitions.") flagLuaMetricsFile = flag.String("lua-metrics-file", "metrics-lua.json", "The JSON file with the lua metric definitions.")
flag_gateway_url = flag.String("gateway-url", "http://fritz.box:49000", "The URL of the FRITZ!Box") flagGatewayURL = flag.String("gateway-url", "http://fritz.box:49000", "The URL of the FRITZ!Box")
flag_gateway_luaurl = flag.String("gateway-luaurl", "http://fritz.box", "The URL of the FRITZ!Box UI") flagGatewayLuaURL = flag.String("gateway-luaurl", "http://fritz.box", "The URL of the FRITZ!Box UI")
flag_gateway_username = flag.String("username", "", "The user for the FRITZ!Box UPnP service") flagUsername = 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") flagPassword = flag.String("password", "", "The password for the FRITZ!Box UPnP service")
) )
var ( var (
collect_errors = prometheus.NewCounter(prometheus.CounterOpts{ collectErrors = prometheus.NewCounter(prometheus.CounterOpts{
Name: "fritzbox_exporter_collect_errors", Name: "fritzbox_exporter_collectErrors",
Help: "Number of collection errors.", Help: "Number of collection errors.",
}) })
) )
var ( var (
lua_collect_errors = prometheus.NewCounter(prometheus.CounterOpts{ luaCollectErrors = prometheus.NewCounter(prometheus.CounterOpts{
Name: "fritzbox_exporter_lua_collect_errors", Name: "fritzbox_exporter_luaCollectErrors",
Help: "Number of lua collection errors.", Help: "Number of lua collection errors.",
}) })
) )
@ -92,13 +92,15 @@ var collectUpnpResultsLoaded = prometheus.NewCounter(prometheus.CounterOpts{
ConstLabels: prometheus.Labels{"Cache": "UPNP"}, ConstLabels: prometheus.Labels{"Cache": "UPNP"},
}) })
type JSON_PromDesc struct { // JSONPromDesc metric description loaded from JSON
type JSONPromDesc struct {
FqName string `json:"fqName"` FqName string `json:"fqName"`
Help string `json:"help"` Help string `json:"help"`
VarLabels []string `json:"varLabels"` VarLabels []string `json:"varLabels"`
FixedLabels map[string]string `json:"fixedLabels"` FixedLabels map[string]string `json:"fixedLabels"`
} }
// ActionArg argument for upnp action
type ActionArg struct { type ActionArg struct {
Name string `json:"Name"` Name string `json:"Name"`
IsIndex bool `json:"IsIndex"` IsIndex bool `json:"IsIndex"`
@ -106,42 +108,46 @@ type ActionArg struct {
Value string `json:"Value"` Value string `json:"Value"`
} }
// Metric upnp metric
type Metric struct { type Metric struct {
// initialized loading JSON // initialized loading JSON
Service string `json:"service"` Service string `json:"service"`
Action string `json:"action"` Action string `json:"action"`
ActionArgument *ActionArg `json:"actionArgument"` ActionArgument *ActionArg `json:"actionArgument"`
Result string `json:"result"` Result string `json:"result"`
OkValue string `json:"okValue"` OkValue string `json:"okValue"`
PromDesc JSON_PromDesc `json:"promDesc"` PromDesc JSONPromDesc `json:"promDesc"`
PromType string `json:"promType"` PromType string `json:"promType"`
CacheEntryTTL int64 `json:"cacheEntryTTL"` CacheEntryTTL int64 `json:"cacheEntryTTL"`
// initialized at startup // initialized at startup
Desc *prometheus.Desc Desc *prometheus.Desc
MetricType prometheus.ValueType MetricType prometheus.ValueType
} }
// LuaTest JSON struct for API tests
type LuaTest struct { type LuaTest struct {
Path string `json:"path"` Path string `json:"path"`
Params string `json:"params"` Params string `json:"params"`
} }
// LuaLabelRename struct
type LuaLabelRename struct { type LuaLabelRename struct {
MatchRegex string `json:"matchRegex"` MatchRegex string `json:"matchRegex"`
RenameLabel string `json:"renameLabel"` RenameLabel string `json:"renameLabel"`
} }
// LuaMetric struct
type LuaMetric struct { type LuaMetric struct {
// initialized loading JSON // initialized loading JSON
Path string `json:"path"` Path string `json:"path"`
Params string `json:"params"` Params string `json:"params"`
ResultPath string `json:"resultPath"` ResultPath string `json:"resultPath"`
ResultKey string `json:"resultKey"` ResultKey string `json:"resultKey"`
OkValue string `json:"okValue"` OkValue string `json:"okValue"`
PromDesc JSON_PromDesc `json:"promDesc"` PromDesc JSONPromDesc `json:"promDesc"`
PromType string `json:"promType"` PromType string `json:"promType"`
CacheEntryTTL int64 `json:"cacheEntryTTL"` CacheEntryTTL int64 `json:"cacheEntryTTL"`
// initialized at startup // initialized at startup
Desc *prometheus.Desc Desc *prometheus.Desc
@ -150,28 +156,30 @@ type LuaMetric struct {
LuaMetricDef lua.LuaMetricValueDefinition LuaMetricDef lua.LuaMetricValueDefinition
} }
// LuaMetricsFile json struct
type LuaMetricsFile struct { type LuaMetricsFile struct {
LabelRenames []LuaLabelRename `json:"labelRenames"` LabelRenames []LuaLabelRename `json:"labelRenames"`
Metrics []*LuaMetric `json:"metrics"` Metrics []*LuaMetric `json:"metrics"`
} }
type UpnpCacheEntry struct { type upnpCacheEntry struct {
Timestamp int64 Timestamp int64
Result *upnp.Result Result *upnp.Result
} }
type LuaCacheEntry struct { type luaCacheEntry struct {
Timestamp int64 Timestamp int64
Result *map[string]interface{} Result *map[string]interface{}
} }
var metrics []*Metric var metrics []*Metric
var luaMetrics []*LuaMetric var luaMetrics []*LuaMetric
var upnpCache map[string]*UpnpCacheEntry var upnpCache map[string]*upnpCacheEntry
var luaCache map[string]*LuaCacheEntry var luaCache map[string]*luaCacheEntry
// FritzboxCollector main struct
type FritzboxCollector struct { type FritzboxCollector struct {
Url string URL string
Gateway string Gateway string
Username string Username string
Password string Password string
@ -185,32 +193,32 @@ type FritzboxCollector struct {
} }
// simple ResponseWriter to collect output // simple ResponseWriter to collect output
type TestResponseWriter struct { type testResponseWriter struct {
header http.Header header http.Header
statusCode int statusCode int
body bytes.Buffer body bytes.Buffer
} }
func (w *TestResponseWriter) Header() http.Header { func (w *testResponseWriter) Header() http.Header {
return w.header return w.header
} }
func (w *TestResponseWriter) Write(b []byte) (int, error) { func (w *testResponseWriter) Write(b []byte) (int, error) {
return w.body.Write(b) return w.body.Write(b)
} }
func (w *TestResponseWriter) WriteHeader(statusCode int) { func (w *testResponseWriter) WriteHeader(statusCode int) {
w.statusCode = statusCode w.statusCode = statusCode
} }
func (w *TestResponseWriter) String() string { func (w *testResponseWriter) String() string {
return w.body.String() return w.body.String()
} }
// 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.Url, fc.Username, fc.Password) root, err := upnp.LoadServices(fc.URL, 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)
@ -227,18 +235,19 @@ func (fc *FritzboxCollector) LoadServices() {
} }
} }
// Describe describe metric
func (fc *FritzboxCollector) Describe(ch chan<- *prometheus.Desc) { func (fc *FritzboxCollector) Describe(ch chan<- *prometheus.Desc) {
for _, m := range metrics { for _, m := range metrics {
ch <- m.Desc ch <- m.Desc
} }
} }
func (fc *FritzboxCollector) ReportMetric(ch chan<- prometheus.Metric, m *Metric, result upnp.Result) { func (fc *FritzboxCollector) reportMetric(ch chan<- prometheus.Metric, m *Metric, result upnp.Result) {
val, ok := result[m.Result] val, ok := result[m.Result]
if !ok { if !ok {
fmt.Printf("%s.%s has no result %s", m.Service, m.Action, m.Result) fmt.Printf("%s.%s has no result %s", m.Service, m.Action, m.Result)
collect_errors.Inc() collectErrors.Inc()
return return
} }
@ -260,7 +269,7 @@ func (fc *FritzboxCollector) ReportMetric(ch chan<- prometheus.Metric, m *Metric
} }
default: default:
fmt.Println("unknown type", val) fmt.Println("unknown type", val)
collect_errors.Inc() collectErrors.Inc()
return return
} }
@ -291,7 +300,7 @@ func (fc *FritzboxCollector) ReportMetric(ch chan<- prometheus.Metric, m *Metric
labels...) labels...)
} }
func (fc *FritzboxCollector) GetActionResult(metric *Metric, actionName string, actionArg *upnp.ActionArgument) (upnp.Result, error) { func (fc *FritzboxCollector) getActionResult(metric *Metric, actionName string, actionArg *upnp.ActionArgument) (upnp.Result, error) {
key := metric.Service + "|" + actionName key := metric.Service + "|" + actionName
@ -304,7 +313,7 @@ func (fc *FritzboxCollector) GetActionResult(metric *Metric, actionName string,
cacheEntry := upnpCache[key] cacheEntry := upnpCache[key]
if cacheEntry == nil { if cacheEntry == nil {
cacheEntry = &UpnpCacheEntry{} cacheEntry = &upnpCacheEntry{}
upnpCache[key] = cacheEntry upnpCache[key] = cacheEntry
} else if now-cacheEntry.Timestamp > metric.CacheEntryTTL { } else if now-cacheEntry.Timestamp > metric.CacheEntryTTL {
cacheEntry.Result = nil cacheEntry.Result = nil
@ -337,6 +346,7 @@ func (fc *FritzboxCollector) GetActionResult(metric *Metric, actionName string,
return *cacheEntry.Result, nil return *cacheEntry.Result, nil
} }
// Collect collect upnp metrics
func (fc *FritzboxCollector) Collect(ch chan<- prometheus.Metric) { func (fc *FritzboxCollector) Collect(ch chan<- prometheus.Metric) {
fc.Lock() fc.Lock()
root := fc.Root root := fc.Root
@ -355,11 +365,11 @@ func (fc *FritzboxCollector) Collect(ch chan<- prometheus.Metric) {
value = aa.Value value = aa.Value
if aa.ProviderAction != "" { if aa.ProviderAction != "" {
provRes, err := fc.GetActionResult(m, aa.ProviderAction, nil) provRes, err := fc.getActionResult(m, aa.ProviderAction, nil)
if err != 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()) 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 continue
} }
@ -367,7 +377,7 @@ func (fc *FritzboxCollector) Collect(ch chan<- prometheus.Metric) {
value, ok = provRes[aa.Value] // Value contains the result name for provider actions value, ok = provRes[aa.Value] // Value contains the result name for provider actions
if !ok { if !ok {
fmt.Printf("provider action %s for %s.%s has no result", m.Service, m.Action, aa.Value) fmt.Printf("provider action %s for %s.%s has no result", m.Service, m.Action, aa.Value)
collect_errors.Inc() collectErrors.Inc()
continue continue
} }
} }
@ -377,21 +387,21 @@ func (fc *FritzboxCollector) Collect(ch chan<- prometheus.Metric) {
count, err := strconv.Atoi(sval) count, err := strconv.Atoi(sval)
if err != nil { if err != nil {
fmt.Println(err.Error()) fmt.Println(err.Error())
collect_errors.Inc() collectErrors.Inc()
continue continue
} }
for i := 0; i < count; i++ { for i := 0; i < count; i++ {
actArg = &upnp.ActionArgument{Name: aa.Name, Value: i} actArg = &upnp.ActionArgument{Name: aa.Name, Value: i}
result, err := fc.GetActionResult(m, m.Action, actArg) result, err := fc.getActionResult(m, m.Action, actArg)
if err != nil { if err != nil {
fmt.Println(err.Error()) fmt.Println(err.Error())
collect_errors.Inc() collectErrors.Inc()
continue continue
} }
fc.ReportMetric(ch, m, result) fc.reportMetric(ch, m, result)
} }
continue continue
@ -400,15 +410,15 @@ func (fc *FritzboxCollector) Collect(ch chan<- prometheus.Metric) {
} }
} }
result, err := fc.GetActionResult(m, m.Action, actArg) result, err := fc.getActionResult(m, m.Action, actArg)
if err != nil { if err != nil {
fmt.Println(err.Error()) fmt.Println(err.Error())
collect_errors.Inc() collectErrors.Inc()
continue continue
} }
fc.ReportMetric(ch, m, result) fc.reportMetric(ch, m, result)
} }
// if lua is enabled now also collect metrics // if lua is enabled now also collect metrics
@ -426,7 +436,7 @@ func (fc *FritzboxCollector) collectLua(ch chan<- prometheus.Metric) {
cacheEntry := luaCache[key] cacheEntry := luaCache[key]
if cacheEntry == nil { if cacheEntry == nil {
cacheEntry = &LuaCacheEntry{} cacheEntry = &luaCacheEntry{}
luaCache[key] = cacheEntry luaCache[key] = cacheEntry
} else if now-cacheEntry.Timestamp > lm.CacheEntryTTL { } else if now-cacheEntry.Timestamp > lm.CacheEntryTTL {
cacheEntry.Result = nil cacheEntry.Result = nil
@ -437,7 +447,7 @@ func (fc *FritzboxCollector) collectLua(ch chan<- prometheus.Metric) {
if err != nil { if err != nil {
fmt.Printf("Error loading %s for %s.%s: %s\n", lm.Path, lm.ResultPath, lm.ResultKey, err.Error()) fmt.Printf("Error loading %s for %s.%s: %s\n", lm.Path, lm.ResultPath, lm.ResultKey, err.Error())
lua_collect_errors.Inc() luaCollectErrors.Inc()
continue continue
} }
@ -445,7 +455,7 @@ func (fc *FritzboxCollector) collectLua(ch chan<- prometheus.Metric) {
data, err = lua.ParseJSON(pageData) data, err = lua.ParseJSON(pageData)
if err != nil { if err != nil {
fmt.Printf("Error parsing JSON from %s for %s.%s: %s\n", lm.Path, lm.ResultPath, lm.ResultKey, err.Error()) fmt.Printf("Error parsing JSON from %s for %s.%s: %s\n", lm.Path, lm.ResultPath, lm.ResultKey, err.Error())
lua_collect_errors.Inc() luaCollectErrors.Inc()
continue continue
} }
@ -460,7 +470,7 @@ func (fc *FritzboxCollector) collectLua(ch chan<- prometheus.Metric) {
if err != nil { if err != nil {
fmt.Printf("Error getting metric values for %s.%s: %s\n", lm.ResultPath, lm.ResultKey, err.Error()) fmt.Printf("Error getting metric values for %s.%s: %s\n", lm.ResultPath, lm.ResultKey, err.Error())
lua_collect_errors.Inc() luaCollectErrors.Inc()
continue continue
} }
@ -500,7 +510,7 @@ func (fc *FritzboxCollector) reportLuaMetric(ch chan<- prometheus.Metric, lm *Lu
} }
func test() { func test() {
root, err := upnp.LoadServices(*flag_gateway_url, *flag_gateway_username, *flag_gateway_password) root, err := upnp.LoadServices(*flagGatewayURL, *flagUsername, *flagPassword)
if err != nil { if err != nil {
panic(err) panic(err)
} }
@ -516,7 +526,7 @@ func test() {
sort.Strings(serviceKeys) sort.Strings(serviceKeys)
for _, k := range serviceKeys { for _, k := range serviceKeys {
s := root.Services[k] s := root.Services[k]
fmt.Printf("Service: %s (Url: %s)\n", k, s.ControlUrl) fmt.Printf("Service: %s (Url: %s)\n", k, s.ControlURL)
actionKeys := []string{} actionKeys := []string{}
for l := range s.Actions { for l := range s.Actions {
@ -571,10 +581,10 @@ func test() {
json.WriteString("\n]") json.WriteString("\n]")
if *flag_jsonout != "" { if *flagJSONOut != "" {
err := ioutil.WriteFile(*flag_jsonout, json.Bytes(), 0644) err := ioutil.WriteFile(*flagJSONOut, json.Bytes(), 0644)
if err != nil { 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())
} }
} }
} }
@ -595,7 +605,7 @@ func testLua() {
} }
// create session struct and init params // create session struct and init params
luaSession := lua.LuaSession{BaseURL: *flag_gateway_luaurl, Username: *flag_gateway_username, Password: *flag_gateway_password} luaSession := lua.LuaSession{BaseURL: *flagGatewayLuaURL, Username: *flagUsername, Password: *flagPassword}
for _, test := range luaTests { for _, test := range luaTests {
fmt.Printf("TESTING: %s (%s)\n", test.Path, test.Params) fmt.Printf("TESTING: %s (%s)\n", test.Path, test.Params)
@ -609,7 +619,8 @@ func testLua() {
fmt.Println(string(pageData)) fmt.Println(string(pageData))
} }
fmt.Println("\n") fmt.Println()
fmt.Println()
} }
} }
@ -629,24 +640,24 @@ func getValueType(vt string) prometheus.ValueType {
func main() { func main() {
flag.Parse() flag.Parse()
u, err := url.Parse(*flag_gateway_url) u, err := url.Parse(*flagGatewayURL)
if err != nil { if err != nil {
fmt.Println("invalid URL:", err) fmt.Println("invalid URL:", err)
return return
} }
if *flag_test { if *flagTest {
test() test()
return return
} }
if *flag_luatest { if *flagLuaTest {
testLua() testLua()
return return
} }
// read metrics // read metrics
jsonData, err := ioutil.ReadFile(*flag_metrics_file) jsonData, err := ioutil.ReadFile(*flagMetricsFile)
if err != nil { if err != nil {
fmt.Println("error reading metric file:", err) fmt.Println("error reading metric file:", err)
return return
@ -659,12 +670,12 @@ func main() {
} }
// create a map for caching results // create a map for caching results
upnpCache = make(map[string]*UpnpCacheEntry) upnpCache = make(map[string]*upnpCacheEntry)
var luaSession *lua.LuaSession var luaSession *lua.LuaSession
var luaLabelRenames *[]lua.LabelRename var luaLabelRenames *[]lua.LabelRename
if !*flag_disable_lua { if !*flagDisableLua {
jsonData, err := ioutil.ReadFile(*flag_lua_metrics_file) jsonData, err := ioutil.ReadFile(*flagLuaMetricsFile)
if err != nil { if err != nil {
fmt.Println("error reading lua metric file:", err) fmt.Println("error reading lua metric file:", err)
return return
@ -678,7 +689,7 @@ func main() {
} }
// create a map for caching results // create a map for caching results
luaCache = make(map[string]*LuaCacheEntry) luaCache = make(map[string]*luaCacheEntry)
// init label renames // init label renames
lblRen := make([]lua.LabelRename, 0) lblRen := make([]lua.LabelRename, 0)
@ -727,9 +738,9 @@ func main() {
} }
luaSession = &lua.LuaSession{ luaSession = &lua.LuaSession{
BaseURL: *flag_gateway_luaurl, BaseURL: *flagGatewayLuaURL,
Username: *flag_gateway_username, Username: *flagUsername,
Password: *flag_gateway_password, Password: *flagPassword,
} }
} }
@ -753,28 +764,28 @@ func main() {
} }
collector := &FritzboxCollector{ collector := &FritzboxCollector{
Url: *flag_gateway_url, URL: *flagGatewayURL,
Gateway: u.Hostname(), Gateway: u.Hostname(),
Username: *flag_gateway_username, Username: *flagUsername,
Password: *flag_gateway_password, Password: *flagPassword,
LuaSession: luaSession, LuaSession: luaSession,
LabelRenames: luaLabelRenames, LabelRenames: luaLabelRenames,
} }
if *flag_collect { if *flagCollect {
collector.LoadServices() collector.LoadServices()
prometheus.MustRegister(collector) prometheus.MustRegister(collector)
prometheus.MustRegister(collect_errors) prometheus.MustRegister(collectErrors)
if luaSession != nil { if luaSession != nil {
prometheus.MustRegister(lua_collect_errors) prometheus.MustRegister(luaCollectErrors)
} }
fmt.Println("collecting metrics via http") fmt.Println("collecting metrics via http")
// simulate HTTP request without starting actual http server // simulate HTTP request without starting actual http server
writer := TestResponseWriter{header: http.Header{}} writer := testResponseWriter{header: http.Header{}}
request := http.Request{} request := http.Request{}
promhttp.Handler().ServeHTTP(&writer, &request) promhttp.Handler().ServeHTTP(&writer, &request)
@ -786,18 +797,18 @@ func main() {
go collector.LoadServices() go collector.LoadServices()
prometheus.MustRegister(collector) prometheus.MustRegister(collector)
prometheus.MustRegister(collect_errors) prometheus.MustRegister(collectErrors)
prometheus.MustRegister(collectUpnpResultsCached) prometheus.MustRegister(collectUpnpResultsCached)
prometheus.MustRegister(collectUpnpResultsLoaded) prometheus.MustRegister(collectUpnpResultsLoaded)
if luaSession != nil { if luaSession != nil {
prometheus.MustRegister(lua_collect_errors) prometheus.MustRegister(luaCollectErrors)
prometheus.MustRegister(collectLuaResultsCached) prometheus.MustRegister(collectLuaResultsCached)
prometheus.MustRegister(collectLuaResultsLoaded) prometheus.MustRegister(collectLuaResultsLoaded)
} }
http.Handle("/metrics", promhttp.Handler()) 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