diff --git a/soap/soap.go b/soap/soap.go index 5072157..33cbe28 100644 --- a/soap/soap.go +++ b/soap/soap.go @@ -9,11 +9,12 @@ import ( "io/ioutil" "net/http" "net/url" + "reflect" ) const ( soapEncodingStyle = "http://schemas.xmlsoap.org/soap/encoding/" - soapPrefix = `` + soapPrefix = xml.Header + `` soapSuffix = `` ) @@ -29,6 +30,8 @@ func NewSOAPClient(endpointURL url.URL) *SOAPClient { } // PerformSOAPAction makes a SOAP request, with the given action. +// inAction and outAction must both be pointers to structs with string fields +// only. func (client *SOAPClient) PerformAction(actionNamespace, actionName string, inAction interface{}, outAction interface{}) error { requestBytes, err := encodeRequestAction(actionNamespace, actionName, inAction) if err != nil { @@ -47,7 +50,7 @@ func (client *SOAPClient) PerformAction(actionNamespace, actionName string, inAc ContentLength: int64(len(requestBytes)), }) if err != nil { - return err + return fmt.Errorf("goupnp: error performing SOAP HTTP request: %v", err) } defer response.Body.Close() if response.StatusCode != 200 { @@ -57,7 +60,7 @@ func (client *SOAPClient) PerformAction(actionNamespace, actionName string, inAc responseEnv := newSOAPEnvelope() decoder := xml.NewDecoder(response.Body) if err := decoder.Decode(responseEnv); err != nil { - return err + return fmt.Errorf("goupnp: error decoding response body: %v", err) } if responseEnv.Body.Fault != nil { @@ -66,7 +69,7 @@ func (client *SOAPClient) PerformAction(actionNamespace, actionName string, inAc if outAction != nil { if err := xml.Unmarshal(responseEnv.Body.RawAction, outAction); err != nil { - return err + return fmt.Errorf("goupnp: error unmarshalling out action: %v, %v", err, responseEnv.Body.RawAction) } } @@ -94,8 +97,7 @@ func encodeRequestAction(actionNamespace, actionName string, inAction interface{ xml.EscapeText(requestBuf, []byte(actionNamespace)) requestBuf.WriteString(`">`) if inAction != nil { - requestEnc := xml.NewEncoder(requestBuf) - if err := requestEnc.Encode(inAction); err != nil { + if err := encodeRequestArgs(requestBuf, inAction); err != nil { return nil, err } } @@ -106,6 +108,32 @@ func encodeRequestAction(actionNamespace, actionName string, inAction interface{ return requestBuf.Bytes(), nil } +func encodeRequestArgs(w *bytes.Buffer, inAction interface{}) error { + in := reflect.Indirect(reflect.ValueOf(inAction)) + if in.Kind() != reflect.Struct { + return fmt.Errorf("goupnp: SOAP inAction is not a struct but of type %v", in.Type()) + } + enc := xml.NewEncoder(w) + nFields := in.NumField() + inType := in.Type() + for i := 0; i < nFields; i++ { + field := inType.Field(i) + argName := field.Name + if nameOverride := field.Tag.Get("soap"); nameOverride != "" { + argName = nameOverride + } + value := in.Field(i) + if value.Kind() != reflect.String { + return fmt.Errorf("goupnp: SOAP arg %q is not of type string, but of type %v", argName, value.Type()) + } + if err := enc.EncodeElement(value.Interface(), xml.StartElement{xml.Name{"", argName}, nil}); err != nil { + return fmt.Errorf("goupnp: error encoding SOAP arg %q: %v", argName, err) + } + } + enc.Flush() + return nil +} + type soapEnvelope struct { XMLName xml.Name `xml:"http://schemas.xmlsoap.org/soap/envelope/ Envelope"` EncodingStyle string `xml:"http://schemas.xmlsoap.org/soap/envelope/ encodingStyle,attr"` diff --git a/soap/soap_test.go b/soap/soap_test.go new file mode 100644 index 0000000..75dbbdb --- /dev/null +++ b/soap/soap_test.go @@ -0,0 +1,85 @@ +package soap + +import ( + "bytes" + "io/ioutil" + "net/http" + "net/url" + "reflect" + "testing" +) + +type capturingRoundTripper struct { + err error + resp *http.Response + capturedReq *http.Request +} + +func (rt *capturingRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + rt.capturedReq = req + return rt.resp, rt.err +} + +func TestActionInputs(t *testing.T) { + url, err := url.Parse("http://example.com/soap") + if err != nil { + t.Fatal(err) + } + rt := &capturingRoundTripper{ + err: nil, + resp: &http.Response{ + StatusCode: 200, + Body: ioutil.NopCloser(bytes.NewBufferString(` + + + + valueA + valueB + + + + `)), + }, + } + client := SOAPClient{ + EndpointURL: *url, + HTTPClient: http.Client{ + Transport: rt, + }, + } + + type In struct { + Foo string + Bar string `soap:"bar"` + } + type Out struct { + A string + B string + } + in := In{"foo", "bar"} + gotOut := Out{} + err = client.PerformAction("mynamespace", "myaction", &in, &gotOut) + if err != nil { + t.Fatal(err) + } + + wantBody := (soapPrefix + + `` + + `foo` + + `bar` + + `` + + soapSuffix) + body, err := ioutil.ReadAll(rt.capturedReq.Body) + if err != nil { + t.Fatal(err) + } + gotBody := string(body) + if wantBody != gotBody { + t.Errorf("Bad request body\nwant: %q\n got: %q", wantBody, gotBody) + } + + wantOut := Out{"valueA", "valueB"} + if !reflect.DeepEqual(wantOut, gotOut) { + t.Errorf("Bad output\nwant: %+v\n got: %+v", wantOut, gotOut) + } +}