-
Notifications
You must be signed in to change notification settings - Fork 120
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
split into smaller files and simplify connection parameter handling.
- Loading branch information
Showing
6 changed files
with
528 additions
and
528 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,133 @@ | ||
package main | ||
|
||
import ( | ||
"fmt" | ||
"sync" | ||
"time" | ||
|
||
upnp "github.com/ndecker/fritzbox_exporter/fritzbox_upnp" | ||
"github.com/prometheus/client_golang/prometheus" | ||
) | ||
|
||
const serviceLoadRetryTime = 1 * time.Minute | ||
|
||
var collectErrors = prometheus.NewCounter(prometheus.CounterOpts{ | ||
Name: "fritzbox_exporter_collect_errors", | ||
Help: "Number of collection errors.", | ||
}) | ||
|
||
type FritzboxCollector struct { | ||
Parameters upnp.ConnectionParameters | ||
sync.Mutex // protects Root | ||
Root *upnp.Root | ||
} | ||
|
||
// LoadServices tries to load the service information. Retries until success. | ||
func (fc *FritzboxCollector) LoadServices() { | ||
for { | ||
root, err := upnp.LoadServices(fc.Parameters) | ||
if err != nil { | ||
fmt.Printf("cannot load services: %s\n", err) | ||
|
||
time.Sleep(serviceLoadRetryTime) | ||
continue | ||
} | ||
|
||
fmt.Printf("services loaded\n") | ||
|
||
fc.Lock() | ||
fc.Root = root | ||
fc.Unlock() | ||
return | ||
} | ||
} | ||
|
||
func (fc *FritzboxCollector) Describe(ch chan<- *prometheus.Desc) { | ||
for _, m := range metrics { | ||
ch <- m.Desc | ||
} | ||
} | ||
|
||
func (fc *FritzboxCollector) Collect(ch chan<- prometheus.Metric) { | ||
fc.Lock() | ||
root := fc.Root | ||
fc.Unlock() | ||
|
||
if root == nil { | ||
return // Services not loaded yet | ||
} | ||
|
||
var err error | ||
var lastService string | ||
var lastMethod string | ||
var lastResult upnp.Result | ||
|
||
for _, m := range metrics { | ||
if m.Service != lastService || m.Action != lastMethod { | ||
service, ok := root.Services[m.Service] | ||
if !ok { | ||
// TODO | ||
fmt.Println("cannot find service", m.Service) | ||
fmt.Println(root.Services) | ||
continue | ||
} | ||
action, ok := service.Actions[m.Action] | ||
if !ok { | ||
// TODO | ||
fmt.Println("cannot find action", m.Action) | ||
continue | ||
} | ||
|
||
lastResult, err = action.Call() | ||
if err != nil { | ||
fmt.Println(err) | ||
collectErrors.Inc() | ||
continue | ||
} | ||
} | ||
|
||
val, ok := lastResult[m.Result] | ||
if !ok { | ||
fmt.Println("result not found", m.Result) | ||
collectErrors.Inc() | ||
continue | ||
} | ||
|
||
floatVal, ok := toFloat(val, m.OkValue) | ||
if !ok { | ||
fmt.Println("cannot convert to float:", val) | ||
collectErrors.Inc() | ||
continue | ||
} | ||
|
||
ch <- prometheus.MustNewConstMetric( | ||
m.Desc, | ||
m.MetricType, | ||
floatVal, | ||
fc.Parameters.Device, | ||
) | ||
} | ||
} | ||
|
||
func toFloat(val any, okValue string) (float64, bool) { | ||
switch tval := val.(type) { | ||
case uint64: | ||
return float64(tval), true | ||
case bool: | ||
if tval { | ||
return 1, true | ||
} else { | ||
return 0, true | ||
} | ||
case string: | ||
if tval == okValue { | ||
return 1, true | ||
} else { | ||
return 0, true | ||
} | ||
default: | ||
return 0, false | ||
|
||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,164 @@ | ||
package fritzbox_upnp | ||
|
||
import ( | ||
"bytes" | ||
"encoding/xml" | ||
"fmt" | ||
"io" | ||
"log" | ||
"net/http" | ||
"strconv" | ||
"strings" | ||
|
||
dac "github.com/123Haynes/go-http-digest-auth-client" | ||
) | ||
|
||
// An UPNP Acton on a service | ||
type Action struct { | ||
service *Service | ||
|
||
Name string `xml:"name"` | ||
Arguments []*Argument `xml:"argumentList>argument"` | ||
ArgumentMap map[string]*Argument // Map of arguments indexed by .Name | ||
} | ||
|
||
// IsGetOnly 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 | ||
} | ||
} | ||
return len(a.Arguments) > 0 | ||
} | ||
|
||
// An Argument to an action | ||
type Argument struct { | ||
Name string `xml:"name"` | ||
Direction string `xml:"direction"` | ||
RelatedStateVariable string `xml:"relatedStateVariable"` | ||
StateVariable *StateVariable | ||
} | ||
|
||
// A state variable that can be manipulated through actions | ||
type StateVariable struct { | ||
Name string `xml:"name"` | ||
DataType string `xml:"dataType"` | ||
DefaultValue string `xml:"defaultValue"` | ||
} | ||
|
||
// The result of a Call() contains all output arguments of the call. | ||
// 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. | ||
type Result map[string]interface{} | ||
|
||
// Call an action. | ||
// Currently only actions without input arguments are supported. | ||
func (a *Action) Call() (Result, error) { | ||
bodystr := fmt.Sprintf(` | ||
<?xml version='1.0' encoding='utf-8'?> | ||
<s:Envelope s:encodingStyle='http://schemas.xmlsoap.org/soap/encoding/' xmlns:s='http://schemas.xmlsoap.org/soap/envelope/'> | ||
<s:Body> | ||
<u:%s xmlns:u='%s' /> | ||
</s:Body> | ||
</s:Envelope> | ||
`, a.Name, a.service.ServiceType) | ||
|
||
url := a.service.Device.root.baseUrl + a.service.ControlUrl | ||
body := strings.NewReader(bodystr) | ||
|
||
req, err := http.NewRequest("POST", url, body) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
action := fmt.Sprintf("%s#%s", a.service.ServiceType, a.Name) | ||
|
||
req.Header.Set("Content-Type", textXml) | ||
req.Header.Set("SoapAction", action) | ||
|
||
t := dac.NewTransport(a.service.Device.root.params.Username, a.service.Device.root.params.Password) | ||
|
||
resp, err := t.RoundTrip(req) | ||
if err != nil { | ||
log.Fatalln(err) | ||
} | ||
defer closeIgnoringError(resp.Body) | ||
|
||
if resp.StatusCode == 401 { | ||
return nil, fmt.Errorf("cannot read service %s: status 401 unauthorized", a.Name) | ||
} | ||
|
||
data, err := io.ReadAll(resp.Body) | ||
if err != nil { | ||
return nil, fmt.Errorf("cannot read request body: %w", err) | ||
} | ||
|
||
return a.parseSoapResponse(data) | ||
|
||
} | ||
|
||
func (a *Action) parseSoapResponse(data []byte) (Result, error) { | ||
res := make(Result) | ||
dec := xml.NewDecoder(bytes.NewReader(data)) | ||
|
||
for { | ||
t, err := dec.Token() | ||
if err == io.EOF { | ||
return res, nil | ||
} | ||
|
||
if err != nil { | ||
return nil, fmt.Errorf("cannot parse soap response: %w; body: %s", err, data) | ||
} | ||
|
||
if se, ok := t.(xml.StartElement); ok { | ||
arg, ok := a.ArgumentMap[se.Name.Local] | ||
|
||
if ok { | ||
t2, err := dec.Token() | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
var val string | ||
switch element := t2.(type) { | ||
case xml.EndElement: | ||
val = "" | ||
case xml.CharData: | ||
val = string(element) | ||
default: | ||
return nil, ErrInvalidSOAPResponse | ||
} | ||
|
||
converted, err := convertResult(val, arg) | ||
if err != nil { | ||
return nil, err | ||
} | ||
res[arg.StateVariable.Name] = converted | ||
} | ||
} | ||
|
||
} | ||
} | ||
|
||
func convertResult(val string, arg *Argument) (interface{}, error) { | ||
switch arg.StateVariable.DataType { | ||
case "string": | ||
return val, nil | ||
case "boolean": | ||
return val == "1", nil | ||
|
||
case "ui1", "ui2", "ui4": | ||
// type ui4 can contain values greater than 2^32! | ||
res, err := strconv.ParseUint(val, 10, 64) | ||
if err != nil { | ||
return nil, err | ||
} | ||
return res, nil | ||
default: | ||
return nil, fmt.Errorf("unknown datatype: %s", arg.StateVariable.DataType) | ||
|
||
} | ||
} |
Oops, something went wrong.