diff --git a/fritzbox_upnp/service.go b/fritzbox_upnp/service.go
index 163dd38..b0a8531 100644
--- a/fritzbox_upnp/service.go
+++ b/fritzbox_upnp/service.go
@@ -16,18 +16,16 @@ package fritzbox_upnp
// limitations under the License.
import (
- "bytes"
"encoding/xml"
"errors"
"fmt"
"io"
- "log"
"net/http"
"crypto/tls"
"strconv"
"strings"
-
- dac "github.com/123Haynes/go-http-digest-auth-client"
+ "crypto/md5"
+ "crypto/rand"
)
// curl http://fritz.box:49000/igddesc.xml
@@ -98,6 +96,30 @@ type Action struct {
ArgumentMap map[string]*Argument // Map of arguments indexed by .Name
}
+// structs to unmarshal SOAP faults
+type SoapEnvelope struct {
+ XMLName xml.Name `xml:"http://schemas.xmlsoap.org/soap/envelope/ Envelope"`
+ Body SoapBody
+}
+type SoapBody struct {
+ XMLName xml.Name `xml:"http://schemas.xmlsoap.org/soap/envelope/ Body"`
+ Fault SoapFault
+}
+type SoapFault struct {
+ XMLName xml.Name `xml:"http://schemas.xmlsoap.org/soap/envelope/ Fault"`
+ FaultCode string `xml:"faultcode"`
+ FaultString string `xml:"faultstring"`
+ Detail FaultDetail `xml:"detail"`
+}
+type FaultDetail struct {
+ UpnpError UpnpError `xml:"UPnPError"`
+}
+type UpnpError struct {
+ ErrorCode int `xml:"errorCode"`
+ ErrorDescription string `xml:"errorDescription"`
+}
+
+
// 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 {
@@ -142,6 +164,8 @@ func (r *Root) load() error {
return err
}
+ defer igddesc.Body.Close()
+
dec := xml.NewDecoder(igddesc.Body)
err = dec.Decode(r)
@@ -162,6 +186,8 @@ func (r *Root) loadTr64() error {
return err
}
+ defer igddesc.Body.Close()
+
dec := xml.NewDecoder(igddesc.Body)
err = dec.Decode(r)
@@ -185,6 +211,8 @@ func (d *Device) fillServices(r *Root) error {
return err
}
+ defer response.Body.Close()
+
var scpd scpdRoot
dec := xml.NewDecoder(response.Body)
@@ -225,17 +253,13 @@ func (d *Device) fillServices(r *Root) error {
return nil
}
-// Call an action.
-// Currently only actions without input arguments are supported.
-func (a *Action) Call() (Result, error) {
- bodystr := fmt.Sprintf(`
-
-
-
-
-
-
- `, a.Name, a.service.ServiceType)
+const SoapActionXML = `` +
+ `` +
+ `` +
+ ``
+
+func (a *Action) createCallHttpRequest() (*http.Request, error) {
+ bodystr := fmt.Sprintf(SoapActionXML, a.Name, a.service.ServiceType)
url := a.service.Device.root.BaseUrl + a.service.ControlUrl
body := strings.NewReader(bodystr)
@@ -248,22 +272,136 @@ func (a *Action) Call() (Result, error) {
action := fmt.Sprintf("%s#%s", a.service.ServiceType, a.Name)
req.Header.Set("Content-Type", text_xml)
- req.Header.Set("SoapAction", action)
+ req.Header.Set("SOAPAction", action)
+
+ return req, nil;
+}
- t := dac.NewTransport(a.service.Device.root.Username, a.service.Device.root.Password)
+// Call an action.
+// Currently only actions without input arguments are supported.
+func (a *Action) Call() (Result, error) {
+ req, err := a.createCallHttpRequest()
- resp, err := t.RoundTrip(req)
if err != nil {
- log.Fatalln(err)
+ return nil, err
+ }
+
+ // first try call without auth header
+ resp, err := http.DefaultClient.Do(req)
+
+ if err != nil {
+ return nil, err
}
- data := new(bytes.Buffer)
- data.ReadFrom(resp.Body)
+ wwwAuth := resp.Header.Get("WWW-Authenticate")
+ if resp.StatusCode == http.StatusUnauthorized {
+ 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 != "" {
+ // 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)
+ if err != nil {
+ return nil, err
+ }
+
+ req, err = a.createCallHttpRequest()
+ if err != nil {
+ return nil, err
+ }
- return a.parseSoapResponse(data)
+ req.Header.Set("Authorization", authHeader)
+
+ resp, err = http.DefaultClient.Do(req)
+
+ if err != nil {
+ return nil, err
+ }
+
+ } else {
+ return nil, errors.New(fmt.Sprintf("Unauthorized, but no username and password given"))
+ }
+ }
+
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ errMsg := fmt.Sprintf("%s (%d)", http.StatusText(resp.StatusCode), resp.StatusCode)
+ if resp.StatusCode == 500 {
+ buf := new(strings.Builder)
+ io.Copy(buf, resp.Body)
+ body := buf.String()
+ //fmt.Println(body)
+
+ var soapEnv SoapEnvelope
+ err := xml.Unmarshal([]byte(body), &soapEnv)
+ if err != nil {
+ errMsg = fmt.Sprintf("error decoding SOAPFault: %s", err.Error())
+ } else {
+ soapFault := soapEnv.Body.Fault
+
+ if soapFault.FaultString == "UPnPError" {
+ upe := soapFault.Detail.UpnpError;
+
+ errMsg = fmt.Sprintf("SAOPFault: %s %d (%s)", soapFault.FaultString, upe.ErrorCode, upe.ErrorDescription)
+ } else {
+ errMsg = fmt.Sprintf("SAOPFault: %s", soapFault.FaultString)
+ }
+ }
+ }
+ return nil, errors.New(errMsg)
+ }
+ return a.parseSoapResponse(resp.Body)
}
+func (a *Action) getDigestAuthHeader(wwwAuth string, username string, password string) (string, error) {
+ // parse www-auth header
+ if ! strings.HasPrefix(wwwAuth, "Digest ") {
+ return "", errors.New(fmt.Sprintf("WWW-Authentication header is not Digest: '%s'", wwwAuth))
+ }
+
+ s := wwwAuth[7:]
+ d := map[string]string{}
+ for _, kv := range strings.Split(s, ",") {
+ parts := strings.SplitN(kv, "=", 2)
+ if len(parts) != 2 {
+ continue
+ }
+ d[strings.Trim(parts[0], "\" ")] = strings.Trim(parts[1], "\" ")
+ }
+
+ if d["algorithm"] == "" {
+ d["algorithm"] = "MD5"
+ } else if d["algorithm"] != "MD5" {
+ return "", errors.New(fmt.Sprintf("digest algorithm not supported: %s != MD5", d["algorithm"]))
+ }
+
+ if d["qop"] != "auth" {
+ return "", errors.New(fmt.Sprintf("digest qop not supported: %s != auth", d["qop"]))
+ }
+
+ // calc h1 and h2
+ ha1 := fmt.Sprintf("%x", md5.Sum([]byte(username + ":" + d["realm"] + ":" + password)))
+
+ ha2 := fmt.Sprintf("%x", md5.Sum([]byte("POST:" + a.service.ControlUrl)))
+
+ cn := make([]byte, 8)
+ rand.Read(cn)
+ cnonce := fmt.Sprintf("%x", cn)
+
+ nCounter := 1
+ nc:=fmt.Sprintf("%08x", nCounter)
+
+ ds := strings.Join([]string{ha1, d["nonce"], nc, cnonce, d["qop"], ha2}, ":")
+ 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",
+ username, d["realm"], d["nonce"], a.service.ControlUrl, cnonce, nc, d["qop"], response, d["algorithm"])
+
+ return authHeader, nil
+}
+
+
func (a *Action) parseSoapResponse(r io.Reader) (Result, error) {
res := make(Result)
dec := xml.NewDecoder(r)
@@ -322,9 +460,17 @@ func convertResult(val string, arg *Argument) (interface{}, error) {
return nil, err
}
return uint64(res), nil
+ case "i4":
+ res, err := strconv.ParseInt(val, 10, 64)
+ if err != nil {
+ return nil, err
+ }
+ return int64(res), nil
+ case "dateTime", "uuid":
+ // data types we don't convert yet
+ return val, nil
default:
- return nil, fmt.Errorf("unknown datatype: %s", arg.StateVariable.DataType)
-
+ return nil, fmt.Errorf("unknown datatype: %s (%s)", arg.StateVariable.DataType, val)
}
}
diff --git a/main.go b/main.go
index 8bc65c6..caf0e47 100644
--- a/main.go
+++ b/main.go
@@ -193,20 +193,20 @@ func test() {
}
for k, s := range root.Services {
- fmt.Printf("Name: %s\n", k)
+ fmt.Printf("Service: %s (Url: %s)\n", k, s.ControlUrl)
for _, a := range s.Actions {
if !a.IsGetOnly() {
continue
}
+ fmt.Printf(" %s\n", a.Name)
res, err := a.Call()
if err != nil {
- fmt.Printf(" %s unexpected error:", a.Name, err)
+ fmt.Printf(" FAILED:%s\n", err.Error())
continue
}
- fmt.Printf(" %s\n", a.Name)
for _, arg := range a.Arguments {
fmt.Printf(" %s: %v\n", arg.RelatedStateVariable, res[arg.StateVariable.Name])
}