Nils Decker
9 years ago
4 changed files with 478 additions and 243 deletions
@ -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(` |
||||
|
<?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["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 |
||||
|
} |
@ -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 "<?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:GetExternalIPAddress xmlns:u='urn:schemas-upnp-org:service:WANIPConnection:1' />
|
|
||||
// </s:Body> </s:Envelope>"
|
|
||||
|
|
||||
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(` |
|
||||
<?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='urn:schemas-upnp-org:service:%s' /> |
|
||||
</s:Body> |
|
||||
</s:Envelope> |
|
||||
`, 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 |
|
||||
} |
|
@ -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, |
|
||||
} |
|
Loading…
Reference in new issue