|
@ -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,7 +36,7 @@ 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") |
|
|
|
|
|
|
|
@ -99,34 +99,33 @@ type Action struct { |
|
|
|
|
|
|
|
|
// An Inüut Argument to pass to an action
|
|
|
// 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
|
|
|
// structs 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 |
|
|
} |
|
|
} |
|
|
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 |
|
|
} |
|
|
} |
|
|
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"` |
|
|
} |
|
|
} |
|
|
type FaultDetail struct { |
|
|
type FaultDetail struct { |
|
|
UpnpError UpnpError `xml:"UPnPError"` |
|
|
UpnpError UpnpError `xml:"UPnPError"` |
|
|
} |
|
|
} |
|
|
type UpnpError struct { |
|
|
type UpnpError struct { |
|
|
ErrorCode int `xml:"errorCode"` |
|
|
ErrorCode int `xml:"errorCode"` |
|
|
ErrorDescription string `xml:"errorDescription"` |
|
|
ErrorDescription string `xml:"errorDescription"` |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 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 { |
|
@ -136,9 +135,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
|
|
@ -172,7 +168,7 @@ func (r *Root) load() error { |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
defer igddesc.Body.Close() |
|
|
defer igddesc.Body.Close() |
|
|
|
|
|
|
|
|
dec := xml.NewDecoder(igddesc.Body) |
|
|
dec := xml.NewDecoder(igddesc.Body) |
|
|
|
|
|
|
|
|
err = dec.Decode(r) |
|
|
err = dec.Decode(r) |
|
@ -262,7 +258,7 @@ func (d *Device) fillServices(r *Root) error { |
|
|
|
|
|
|
|
|
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>` |
|
@ -287,18 +283,18 @@ 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
|
|
|
var authHeader = "" |
|
|
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 |
|
@ -308,7 +304,7 @@ func (a *Action) Call(actionArg *ActionArgument) (Result, error) { |
|
|
if authHeader != "" { |
|
|
if authHeader != "" { |
|
|
req.Header.Set("Authorization", authHeader) |
|
|
req.Header.Set("Authorization", authHeader) |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
// first try call without auth header
|
|
|
// first try call without auth header
|
|
|
resp, err := http.DefaultClient.Do(req) |
|
|
resp, err := http.DefaultClient.Do(req) |
|
|
|
|
|
|
|
@ -318,8 +314,8 @@ 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) |
|
@ -327,24 +323,24 @@ func (a *Action) Call(actionArg *ActionArgument) (Result, error) { |
|
|
return nil, errors.New(fmt.Sprintf("%s: %s", a.Name, err.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 { |
|
|
if err != nil { |
|
|
return nil, errors.New(fmt.Sprintf("%s: %s", a.Name, err.Error)) |
|
|
return nil, errors.New(fmt.Sprintf("%s: %s", a.Name, err.Error)) |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
req.Header.Set("Authorization", authHeader) |
|
|
req.Header.Set("Authorization", authHeader) |
|
|
|
|
|
|
|
|
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, errors.New(fmt.Sprintf("%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, errors.New(fmt.Sprintf("%s: Unauthorized, but no username and password given", a.Name)) |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
defer resp.Body.Close() |
|
|
defer resp.Body.Close() |
|
|
|
|
|
|
|
|
if resp.StatusCode != http.StatusOK { |
|
|
if resp.StatusCode != http.StatusOK { |
|
@ -354,22 +350,22 @@ func (a *Action) Call(actionArg *ActionArgument) (Result, error) { |
|
|
io.Copy(buf, resp.Body) |
|
|
io.Copy(buf, resp.Body) |
|
|
body := buf.String() |
|
|
body := buf.String() |
|
|
//fmt.Println(body)
|
|
|
//fmt.Println(body)
|
|
|
|
|
|
|
|
|
var soapEnv SoapEnvelope |
|
|
var soapEnv SoapEnvelope |
|
|
err := xml.Unmarshal([]byte(body), &soapEnv) |
|
|
err := xml.Unmarshal([]byte(body), &soapEnv) |
|
|
if err != nil { |
|
|
if err != nil { |
|
|
errMsg = fmt.Sprintf("error decoding SOAPFault: %s", err.Error()) |
|
|
errMsg = fmt.Sprintf("error decoding SOAPFault: %s", err.Error()) |
|
|
} else { |
|
|
} else { |
|
|
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 { |
|
|
errMsg = fmt.Sprintf("SAOPFault: %s", soapFault.FaultString) |
|
|
errMsg = fmt.Sprintf("SAOPFault: %s", soapFault.FaultString) |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
return nil, errors.New(fmt.Sprintf("%s: %s", a.Name, errMsg)) |
|
|
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) { |
|
|
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 "", errors.New(fmt.Sprintf("WWW-Authentication header is not Digest: '%s'", wwwAuth)) |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
s := wwwAuth[7:] |
|
|
s := wwwAuth[7:] |
|
|
d := map[string]string{} |
|
|
d := map[string]string{} |
|
|
for _, kv := range strings.Split(s, ",") { |
|
|
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], "\" ") |
|
|
d[strings.Trim(parts[0], "\" ")] = strings.Trim(parts[1], "\" ") |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
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 "", errors.New(fmt.Sprintf("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 "", errors.New(fmt.Sprintf("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) |
|
@ -482,7 +477,7 @@ func convertResult(val string, arg *Argument) (interface{}, error) { |
|
|
if err != nil { |
|
|
if err != nil { |
|
|
return nil, err |
|
|
return nil, err |
|
|
} |
|
|
} |
|
|
return uint64(res), nil |
|
|
return res, nil |
|
|
case "i4": |
|
|
case "i4": |
|
|
res, err := strconv.ParseInt(val, 10, 64) |
|
|
res, err := strconv.ParseInt(val, 10, 64) |
|
|
if err != nil { |
|
|
if err != nil { |
|
@ -491,7 +486,7 @@ func convertResult(val string, arg *Argument) (interface{}, error) { |
|
|
return int64(res), nil |
|
|
return int64(res), nil |
|
|
case "dateTime", "uuid": |
|
|
case "dateTime", "uuid": |
|
|
// data types we don't convert yet
|
|
|
// data types we don't convert yet
|
|
|
return val, nil |
|
|
return val, nil |
|
|
default: |
|
|
default: |
|
|
return nil, fmt.Errorf("unknown datatype: %s (%s)", arg.StateVariable.DataType, val) |
|
|
return nil, fmt.Errorf("unknown datatype: %s (%s)", arg.StateVariable.DataType, val) |
|
|
} |
|
|
} |
|
|