Correct the encoding of SOAP action arguments.

Also adds a test for this, and the decoding of the response arguments.
This commit is contained in:
John Beisley 2014-06-06 21:21:13 +01:00
parent 5c55e50548
commit 788bb66b80
2 changed files with 119 additions and 6 deletions

View File

@ -9,11 +9,12 @@ import (
"io/ioutil"
"net/http"
"net/url"
"reflect"
)
const (
soapEncodingStyle = "http://schemas.xmlsoap.org/soap/encoding/"
soapPrefix = `<?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:Body>`
soapPrefix = xml.Header + `<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><s:Body>`
soapSuffix = `</s:Body></s:Envelope>`
)
@ -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"`

85
soap/soap_test.go Normal file
View File

@ -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(`
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Body>
<u:myactionResponse xmlns:u="mynamespace">
<A>valueA</A>
<B>valueB</B>
</u:myactionResponse>
</s:Body>
</s:Envelope>
`)),
},
}
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 +
`<u:myaction xmlns:u="mynamespace">` +
`<Foo>foo</Foo>` +
`<bar>bar</bar>` +
`</u:myaction>` +
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)
}
}