From 0e0d9f0617195c19eee4829d65ebb530816ce20f Mon Sep 17 00:00:00 2001 From: sberk42 Date: Sat, 20 Jun 2020 08:13:17 +0200 Subject: [PATCH] multiple bug fixes, replaced digest auth coding --- fritzbox_upnp/service.go | 194 ++++++++++++++++++++++++++++++++++----- main.go | 6 +- 2 files changed, 173 insertions(+), 27 deletions(-) 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]) }