diff --git a/fritzbox_upnp/service.go b/fritzbox_upnp/service.go
new file mode 100644
index 0000000..1e2c5cb
--- /dev/null
+++ b/fritzbox_upnp/service.go
@@ -0,0 +1,281 @@
+package fritzbox_upnp
+
+import (
+ "bytes"
+ "encoding/xml"
+ "errors"
+ "fmt"
+ "io"
+ "net/http"
+ "strconv"
+ "strings"
+)
+
+// curl http://fritz.box:49000/igddesc.xml
+// curl http://fritz.box:49000/any.xml
+// curl http://fritz.box:49000/igdconnSCPD.xml
+// curl http://fritz.box:49000/igdicfgSCPD.xml
+// curl http://fritz.box:49000/igddslSCPD.xml
+// curl http://fritz.box:49000/igd2ipv6fwcSCPD.xml
+
+const text_xml = `text/xml; charset="utf-8"`
+
+var ErrResultWithoutChardata = errors.New("result without chardata")
+
+type Root struct {
+ BaseUrl string
+ Device UpnpDevice `xml:"device"`
+ Services map[string]*UpnpService
+}
+
+type UpnpDevice struct {
+ root *Root
+
+ DeviceType string `xml:"deviceType"`
+ FriendlyName string `xml:"friendlyName"`
+ Manufacturer string `xml:"manufacturer"`
+ ManufacturerUrl string `xml:"manufacturerURL"`
+ ModelDescription string `xml:"modelDescription"`
+ ModelName string `xml:"modelName"`
+ ModelNumber string `xml:"modelNumber"`
+ ModelUrl string `xml:"modelURL"`
+ UDN string `xml:"UDN"`
+
+ Services []*UpnpService `xml:"serviceList>service"`
+ Devices []*UpnpDevice `xml:"deviceList>device"`
+
+ PresentationUrl string `xml:"presentationURL"`
+}
+
+type UpnpService struct {
+ Device *UpnpDevice
+
+ ServiceType string `xml:"serviceType"`
+ ServiceId string `xml:"serviceId"`
+ ControlUrl string `xml:"controlURL"`
+ EventSubUrl string `xml:"eventSubURL"`
+ SCPDUrl string `xml:"SCPDURL"`
+
+ Actions map[string]*UpnpAction
+ StateVariables []*UpnpStateVariable
+}
+
+type upnpScpd struct {
+ Actions []*UpnpAction `xml:"actionList>action"`
+ StateVariables []*UpnpStateVariable `xml:"serviceStateTable>stateVariable"`
+}
+
+type UpnpAction struct {
+ service *UpnpService
+
+ Name string `xml:"name"`
+ Arguments []*Argument `xml:"argumentList>argument"`
+ ArgumentMap map[string]*Argument
+}
+
+func (a *UpnpAction) IsGetOnly() bool {
+ for _, a := range a.Arguments {
+ if a.Direction == "in" {
+ return false
+ }
+ }
+ return len(a.Arguments) > 0
+}
+
+type Argument struct {
+ Name string `xml:"name"`
+ Direction string `xml:"direction"`
+ RelatedStateVariable string `xml:"relatedStateVariable"`
+ StateVariable *UpnpStateVariable
+}
+
+type UpnpStateVariable struct {
+ Name string `xml:"name"`
+ DataType string `xml:"dataType"`
+ DefaultValue string `xml:"defaultValue"`
+}
+
+type Result map[string]interface{}
+
+func (r *Root) load() error {
+ igddesc, err := http.Get(
+ fmt.Sprintf("%s/igddesc.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]*UpnpService)
+ return r.Device.fillServices(r)
+}
+
+func (d *UpnpDevice) fillServices(r *Root) error {
+ d.root = r
+
+ for _, s := range d.Services {
+ s.Device = d
+
+ response, err := http.Get(r.BaseUrl + s.SCPDUrl)
+ if err != nil {
+ return err
+ }
+
+ var scpd upnpScpd
+
+ dec := xml.NewDecoder(response.Body)
+ err = dec.Decode(&scpd)
+ if err != nil {
+ return err
+ }
+
+ s.Actions = make(map[string]*UpnpAction)
+ for _, a := range scpd.Actions {
+ s.Actions[a.Name] = a
+ }
+ s.StateVariables = scpd.StateVariables
+
+ for _, a := range s.Actions {
+ a.service = s
+ a.ArgumentMap = make(map[string]*Argument)
+
+ for _, arg := range a.Arguments {
+ for _, svar := range s.StateVariables {
+ if arg.RelatedStateVariable == svar.Name {
+ arg.StateVariable = svar
+ }
+ }
+
+ a.ArgumentMap[arg.Name] = arg
+ }
+ }
+
+ r.Services[s.ServiceType] = s
+ }
+ for _, d2 := range d.Devices {
+ err := d2.fillServices(r)
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func (a *UpnpAction) Call() (Result, error) {
+ bodystr := fmt.Sprintf(`
+
+
+
+
+
+
+ `, 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["Content-Type"] = []string{text_xml}
+ req.Header["SoapAction"] = []string{action}
+
+ resp, err := http.DefaultClient.Do(req)
+ if err != nil {
+ return nil, err
+ }
+
+ data := new(bytes.Buffer)
+ data.ReadFrom(resp.Body)
+
+ // fmt.Printf(data.String())
+ return a.parseSoapResponse(data)
+
+}
+
+func (a *UpnpAction) parseSoapResponse(r io.Reader) (Result, error) {
+ res := make(Result)
+ dec := xml.NewDecoder(r)
+
+ for {
+ t, err := dec.Token()
+ if err == io.EOF {
+ return res, nil
+ }
+
+ if err != nil {
+ return nil, err
+ }
+
+ 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, ErrResultWithoutChardata
+ }
+
+ 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 bool(val == "1"), nil
+
+ case "ui1", "ui2", "ui4":
+ res, err := strconv.ParseUint(val, 10, 32)
+ if err != nil {
+ return nil, err
+ }
+ return uint32(res), nil
+ default:
+ return nil, fmt.Errorf("unknown datatype: %s", arg.StateVariable.DataType)
+
+ }
+}
+
+func LoadServices(device string, port uint16) (*Root, error) {
+ var root = &Root{
+ BaseUrl: fmt.Sprintf("http://%s:%d", device, port),
+ }
+
+ err := root.load()
+ if err != nil {
+ return nil, err
+ }
+
+ return root, nil
+}
diff --git a/fritzbox_upnp/upnp.go b/fritzbox_upnp/upnp.go
deleted file mode 100644
index d015d00..0000000
--- a/fritzbox_upnp/upnp.go
+++ /dev/null
@@ -1,124 +0,0 @@
-package fritzbox_upnp
-
-import (
- "bytes"
- "encoding/xml"
- "errors"
- "fmt"
- "io"
- "net/http"
- "strconv"
- "strings"
-)
-
-const text_xml = `text/xml; charset="utf-8"`
-
-var (
- ErrResultNotFound = errors.New("result not found")
- ErrResultWithoutChardata = errors.New("result without chardata")
-)
-
-// curl "http://fritz.box:49000/igdupnp/control/WANIPConn1"
-// -H "Content-Type: text/xml; charset="utf-8""
-// -H "SoapAction:urn:schemas-upnp-org:service:WANIPConnection:1#GetExternalIPAddress"
-// -d "
-//
-//
-//
-// "
-
-type UpnpValue struct {
- Path string
- Service string
- Method string
- RetTag string
-
- ShortName string
- Help string
-}
-
-func (v *UpnpValue) query(device string, port uint16) (string, error) {
- url := fmt.Sprintf("http://%s:%d%s", device, port, v.Path)
-
- bodystr := fmt.Sprintf(`
-
-
-
-
-
-
- `, v.Method, v.Service)
-
- body := strings.NewReader(bodystr)
-
- req, err := http.NewRequest("POST", url, body)
- if err != nil {
- return "", err
- }
-
- action := fmt.Sprintf("urn:schemas-upnp-org:service:%s#%s", v.Service, v.Method)
-
- req.Header["Content-Type"] = []string{text_xml}
- req.Header["SoapAction"] = []string{action}
-
- resp, err := http.DefaultClient.Do(req)
- if err != nil {
- return "", err
- }
-
- data := new(bytes.Buffer)
- data.ReadFrom(resp.Body)
-
- // fmt.Printf(data.String())
-
- dec := xml.NewDecoder(data)
-
- for {
- t, err := dec.Token()
- if err == io.EOF {
- return "", ErrResultNotFound
- }
-
- if err != nil {
- return "", err
- }
-
- if se, ok := t.(xml.StartElement); ok {
- if se.Name.Local == v.RetTag {
- t2, err := dec.Token()
- if err != nil {
- return "", err
- }
-
- data, ok := t2.(xml.CharData)
- if !ok {
- return "", ErrResultWithoutChardata
- }
- return string(data), nil
- }
- }
-
- }
-}
-
-type UpnpValueString struct{ UpnpValue }
-
-func (v *UpnpValueString) Query(device string, port uint16) (string, error) {
- return v.query(device, port)
-}
-
-type UpnpValueUint struct{ UpnpValue }
-
-func (v *UpnpValueUint) Query(device string, port uint16) (uint64, error) {
- strval, err := v.query(device, port)
- if err != nil {
- return 0, err
- }
-
- val, err := strconv.ParseUint(strval, 10, 64)
- if err != nil {
- return 0, err
- }
-
- return val, nil
-}
diff --git a/fritzbox_upnp/values.go b/fritzbox_upnp/values.go
deleted file mode 100644
index 656cb89..0000000
--- a/fritzbox_upnp/values.go
+++ /dev/null
@@ -1,67 +0,0 @@
-package fritzbox_upnp
-
-// curl http://fritz.box:49000/igddesc.xml
-// curl http://fritz.box:49000/any.xml
-// curl http://fritz.box:49000/igdconnSCPD.xml
-// curl http://fritz.box:49000/igdicfgSCPD.xml
-// curl http://fritz.box:49000/igddslSCPD.xml
-// curl http://fritz.box:49000/igd2ipv6fwcSCPD.xml
-
-var (
- WAN_IP = UpnpValueString{UpnpValue{
- Path: "/igdupnp/control/WANIPConn1",
- Service: "WANIPConnection:1",
- Method: "GetExternalIPAddress",
- RetTag: "NewExternalIPAddress",
-
- ShortName: "wan_ip",
- Help: "WAN IP Adress",
- }}
-
- WAN_Packets_Received = UpnpValueUint{UpnpValue{
- Path: "/igdupnp/control/WANCommonIFC1",
- Service: "WANCommonInterfaceConfig:1",
- Method: "GetTotalPacketsReceived",
- RetTag: "NewTotalPacketsReceived",
-
- ShortName: "packets_received",
- Help: "packets received on gateway WAN interface",
- }}
-
- WAN_Packets_Sent = UpnpValueUint{UpnpValue{
- Path: "/igdupnp/control/WANCommonIFC1",
- Service: "WANCommonInterfaceConfig:1",
- Method: "GetTotalPacketsSent",
- RetTag: "NewTotalPacketsSent",
-
- ShortName: "packets_sent",
- Help: "packets sent on gateway WAN interface",
- }}
-
- WAN_Bytes_Received = UpnpValueUint{UpnpValue{
- Path: "/igdupnp/control/WANCommonIFC1",
- Service: "WANCommonInterfaceConfig:1",
- Method: "GetAddonInfos",
- RetTag: "NewTotalBytesReceived",
-
- ShortName: "bytes_received",
- Help: "bytes received on gateway WAN interface",
- }}
-
- WAN_Bytes_Sent = UpnpValueUint{UpnpValue{
- Path: "/igdupnp/control/WANCommonIFC1",
- Service: "WANCommonInterfaceConfig:1",
- Method: "GetAddonInfos",
- RetTag: "NewTotalBytesSent",
-
- ShortName: "bytes_sent",
- Help: "bytes sent on gateway WAN interface",
- }}
-)
-
-var Values = []UpnpValueUint{
- WAN_Packets_Received,
- WAN_Packets_Sent,
- WAN_Bytes_Received,
- WAN_Bytes_Sent,
-}
diff --git a/main.go b/main.go
index 8339172..7f0dfb9 100644
--- a/main.go
+++ b/main.go
@@ -25,87 +25,232 @@ var (
})
)
-type UpnpMetric struct {
- upnp.UpnpValueUint
- *prometheus.Desc
-}
+type Metric struct {
+ Service string
+ Action string
+ Result string
+ OkValue string
-func (m UpnpMetric) Describe(ch chan<- *prometheus.Desc) {
- ch <- m.Desc
+ Desc *prometheus.Desc
}
-func (m UpnpMetric) Collect(gateway string, port uint16, ch chan<- prometheus.Metric) error {
- val, err := m.Query(gateway, port)
- if err != nil {
- return err
- }
-
- ch <- prometheus.MustNewConstMetric(
- m.Desc,
- prometheus.CounterValue,
- float64(val),
- gateway,
- )
- return nil
-}
-
-func NewUpnpMetric(v upnp.UpnpValueUint) UpnpMetric {
- return UpnpMetric{
- v,
- prometheus.NewDesc(
- prometheus.BuildFQName("gateway", "wan", v.ShortName),
- v.Help,
+var metrics = []*Metric{
+ {
+ Service: "urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1",
+ Action: "GetTotalPacketsReceived",
+ Result: "TotalPacketsReceived",
+ Desc: prometheus.NewDesc(
+ "gateway_wan_packets_received",
+ "packets received on gateway WAN interface",
[]string{"gateway"},
nil,
),
- }
+ },
+ {
+ Service: "urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1",
+ Action: "GetTotalPacketsSent",
+ Result: "TotalPacketsSent",
+ Desc: prometheus.NewDesc(
+ "gateway_wan_packets_sent",
+ "packets sent on gateway WAN interface",
+ []string{"gateway"},
+ nil,
+ ),
+ },
+ {
+ Service: "urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1",
+ Action: "GetAddonInfos",
+ Result: "TotalBytesReceived",
+ Desc: prometheus.NewDesc(
+ "gateway_wan_bytes_received",
+ "bytes received on gateway WAN interface",
+ []string{"gateway"},
+ nil,
+ ),
+ },
+ {
+ Service: "urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1",
+ Action: "GetAddonInfos",
+ Result: "TotalBytesSent",
+ Desc: prometheus.NewDesc(
+ "gateway_wan_bytes_sent",
+ "bytes sent on gateway WAN interface",
+ []string{"gateway"},
+ nil,
+ ),
+ },
+ {
+ Service: "urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1",
+ Action: "GetCommonLinkProperties",
+ Result: "Layer1UpstreamMaxBitRate",
+ Desc: prometheus.NewDesc(
+ "gateway_wan_layer1_upstream_max_bitrate",
+ "Layer1 upstream max bitrate",
+ []string{"gateway"},
+ nil,
+ ),
+ },
+ {
+ Service: "urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1",
+ Action: "GetCommonLinkProperties",
+ Result: "Layer1DownstreamMaxBitRate",
+ Desc: prometheus.NewDesc(
+ "gateway_wan_layer1_downstream_max_bitrate",
+ "Layer1 downstream max bitrate",
+ []string{"gateway"},
+ nil,
+ ),
+ },
+ {
+ Service: "urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1",
+ Action: "GetCommonLinkProperties",
+ Result: "PhysicalLinkStatus",
+ OkValue: "Up",
+ Desc: prometheus.NewDesc(
+ "gateway_wan_layer1_link_status",
+ "Status of physical link (Up = 1)",
+ []string{"gateway"},
+ nil,
+ ),
+ },
+ {
+ Service: "urn:schemas-upnp-org:service:WANIPConnection:1",
+ Action: "GetStatusInfo",
+ Result: "ConnectionStatus",
+ OkValue: "Connected",
+ Desc: prometheus.NewDesc(
+ "gateway_wan_connection_status",
+ "WAN connection status (Connected = 1)",
+ []string{"gateway"},
+ nil,
+ ),
+ },
+ {
+ Service: "urn:schemas-upnp-org:service:WANIPConnection:1",
+ Action: "GetStatusInfo",
+ Result: "Uptime",
+ Desc: prometheus.NewDesc(
+ "gateway_wan_connection_uptime_seconds",
+ "WAN connection uptime",
+ []string{"gateway"},
+ nil,
+ ),
+ },
}
type FritzboxCollector struct {
- gateway string
- port uint16
- metrics []UpnpMetric
+ Root *upnp.Root
+ Gateway string
}
func (fc *FritzboxCollector) Describe(ch chan<- *prometheus.Desc) {
- for _, m := range fc.metrics {
- m.Describe(ch)
+ for _, m := range metrics {
+ ch <- m.Desc
}
}
func (fc *FritzboxCollector) Collect(ch chan<- prometheus.Metric) {
- for _, m := range fc.metrics {
- err := m.Collect(fc.gateway, fc.port, ch)
- if err != nil {
- collect_errors.Inc()
+ var err error
+ var last_service string
+ var last_method string
+ var last_result upnp.Result
+
+ for _, m := range metrics {
+ if m.Service != last_service || m.Action != last_method {
+ service, ok := fc.Root.Services[m.Service]
+ if !ok {
+ // TODO
+ fmt.Println("cannot find service", m.Service)
+ fmt.Println(fc.Root.Services)
+ continue
+ }
+ action, ok := service.Actions[m.Action]
+ if !ok {
+ // TODO
+ fmt.Println("cannot find action", m.Action)
+ continue
+ }
+
+ last_result, err = action.Call()
+ if err != nil {
+ fmt.Println(err)
+ collect_errors.Inc()
+ continue
+ }
+ }
+
+ val, ok := last_result[m.Result]
+ if !ok {
+ fmt.Println("result not found", m.Result)
+ collect_errors.Inc()
+ continue
+ }
+
+ var floatval float64
+ switch tval := val.(type) {
+ case uint32:
+ floatval = float64(tval)
+ case bool:
+ if tval {
+ floatval = 1
+ } else {
+ floatval = 0
+ }
+ case string:
+ if tval == m.OkValue {
+ floatval = 1
+ } else {
+ floatval = 0
+ }
+ default:
+ fmt.Println("unknown", val)
+ collect_errors.Inc()
+ continue
+
}
+
+ ch <- prometheus.MustNewConstMetric(
+ m.Desc,
+ prometheus.CounterValue, // TODO
+ floatval,
+ fc.Gateway,
+ )
+
}
}
func main() {
flag.Parse()
+ root, err := upnp.LoadServices(*flag_gateway_address, uint16(*flag_gateway_port))
+ if err != nil {
+ panic(err)
+ }
+
if *flag_test {
- for _, v := range upnp.Values {
- res, err := v.Query(*flag_gateway_address, uint16(*flag_gateway_port))
- 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
+ }
+
+ res, err := a.Call()
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Printf(" %s\n", a.Name)
+ for _, arg := range a.Arguments {
+ fmt.Printf(" %s - %s: %v\n", arg.RelatedStateVariable, arg.StateVariable.DataType, res[arg.StateVariable.Name])
+ }
}
- fmt.Printf("%s: %d\n", v.ShortName, res)
}
- return
- }
- metrics := make([]UpnpMetric, len(upnp.Values))
- for _, v := range upnp.Values {
- metrics = append(metrics, NewUpnpMetric(v))
+ return
}
- prometheus.MustRegister(&FritzboxCollector{
- *flag_gateway_address,
- uint16(*flag_gateway_port),
- metrics,
- })
+ prometheus.MustRegister(&FritzboxCollector{root, *flag_gateway_address})
// Since we are dealing with custom Collector implementations, it might
// be a good idea to enable the collect checks in the registry.
prometheus.EnableCollectChecks(true)