Browse Source

multiple bug fixes, replaced digest auth coding

pull/1/head
sberk42 5 years ago
parent
commit
0e0d9f0617
  1. 194
      fritzbox_upnp/service.go
  2. 6
      main.go

194
fritzbox_upnp/service.go

@ -16,18 +16,16 @@ package fritzbox_upnp
// limitations under the License. // limitations under the License.
import ( import (
"bytes"
"encoding/xml" "encoding/xml"
"errors" "errors"
"fmt" "fmt"
"io" "io"
"log"
"net/http" "net/http"
"crypto/tls" "crypto/tls"
"strconv" "strconv"
"strings" "strings"
"crypto/md5"
dac "github.com/123Haynes/go-http-digest-auth-client" "crypto/rand"
) )
// curl http://fritz.box:49000/igddesc.xml // curl http://fritz.box:49000/igddesc.xml
@ -98,6 +96,30 @@ type Action struct {
ArgumentMap map[string]*Argument // Map of arguments indexed by .Name 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. // 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 {
@ -142,6 +164,8 @@ func (r *Root) load() error {
return err return err
} }
defer igddesc.Body.Close()
dec := xml.NewDecoder(igddesc.Body) dec := xml.NewDecoder(igddesc.Body)
err = dec.Decode(r) err = dec.Decode(r)
@ -162,6 +186,8 @@ func (r *Root) loadTr64() error {
return err return err
} }
defer igddesc.Body.Close()
dec := xml.NewDecoder(igddesc.Body) dec := xml.NewDecoder(igddesc.Body)
err = dec.Decode(r) err = dec.Decode(r)
@ -185,6 +211,8 @@ func (d *Device) fillServices(r *Root) error {
return err return err
} }
defer response.Body.Close()
var scpd scpdRoot var scpd scpdRoot
dec := xml.NewDecoder(response.Body) dec := xml.NewDecoder(response.Body)
@ -225,17 +253,13 @@ func (d *Device) fillServices(r *Root) error {
return nil return nil
} }
// Call an action. const SoapActionXML = `<?xml version="1.0" encoding="utf-8"?>` +
// Currently only actions without input arguments are supported. `<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">` +
func (a *Action) Call() (Result, error) { `<s:Body><u:%s xmlns:u=%s /></s:Body>` +
bodystr := fmt.Sprintf(` `</s:Envelope>`
<?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/'> func (a *Action) createCallHttpRequest() (*http.Request, error) {
<s:Body> bodystr := fmt.Sprintf(SoapActionXML, a.Name, a.service.ServiceType)
<u:%s xmlns:u='%s' />
</s:Body>
</s:Envelope>
`, a.Name, a.service.ServiceType)
url := a.service.Device.root.BaseUrl + a.service.ControlUrl url := a.service.Device.root.BaseUrl + a.service.ControlUrl
body := strings.NewReader(bodystr) body := strings.NewReader(bodystr)
@ -248,22 +272,136 @@ func (a *Action) Call() (Result, error) {
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", 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 { 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) wwwAuth := resp.Header.Get("WWW-Authenticate")
data.ReadFrom(resp.Body) 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) { func (a *Action) parseSoapResponse(r io.Reader) (Result, error) {
res := make(Result) res := make(Result)
dec := xml.NewDecoder(r) dec := xml.NewDecoder(r)
@ -322,9 +460,17 @@ func convertResult(val string, arg *Argument) (interface{}, error) {
return nil, err return nil, err
} }
return uint64(res), nil 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: default:
return nil, fmt.Errorf("unknown datatype: %s", arg.StateVariable.DataType) return nil, fmt.Errorf("unknown datatype: %s (%s)", arg.StateVariable.DataType, val)
} }
} }

6
main.go

@ -193,20 +193,20 @@ func test() {
} }
for k, s := range root.Services { 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 { for _, a := range s.Actions {
if !a.IsGetOnly() { if !a.IsGetOnly() {
continue continue
} }
fmt.Printf(" %s\n", a.Name)
res, err := a.Call() res, err := a.Call()
if err != nil { if err != nil {
fmt.Printf(" %s unexpected error:", a.Name, err) fmt.Printf(" FAILED:%s\n", err.Error())
continue continue
} }
fmt.Printf(" %s\n", a.Name)
for _, arg := range a.Arguments { for _, arg := range a.Arguments {
fmt.Printf(" %s: %v\n", arg.RelatedStateVariable, res[arg.StateVariable.Name]) fmt.Printf(" %s: %v\n", arg.RelatedStateVariable, res[arg.StateVariable.Name])
} }

Loading…
Cancel
Save