From 991e174e2e678f42c106d9f3587cebff65be9a70 Mon Sep 17 00:00:00 2001 From: John Beisley Date: Tue, 7 Nov 2017 18:19:10 -0500 Subject: [PATCH] Add workaround for SOAP server XML decoding limitations. --- soap/soap.go | 40 ++++++++++++++++++++++++++++++++++++++-- soap/soap_test.go | 28 +++++++++++++++++++++++++++- 2 files changed, 65 insertions(+), 3 deletions(-) diff --git a/soap/soap.go b/soap/soap.go index 8156107..29e89f2 100644 --- a/soap/soap.go +++ b/soap/soap.go @@ -10,6 +10,7 @@ import ( "net/http" "net/url" "reflect" + "regexp" ) const ( @@ -126,14 +127,49 @@ func encodeRequestArgs(w *bytes.Buffer, inAction interface{}) error { 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) + elem := xml.StartElement{xml.Name{"", argName}, nil} + if err := enc.EncodeToken(elem); err != nil { + return fmt.Errorf("goupnp: error encoding start element for SOAP arg %q: %v", argName, err) + } + if err := enc.Flush(); err != nil { + return fmt.Errorf("goupnp: error flushing start element for SOAP arg %q: %v", argName, err) + } + if _, err := w.Write([]byte(escapeXMLText(value.Interface().(string)))); err != nil { + return fmt.Errorf("goupnp: error writing value for SOAP arg %q: %v", argName, err) + } + if err := enc.EncodeToken(elem.End()); err != nil { + return fmt.Errorf("goupnp: error encoding end element for SOAP arg %q: %v", argName, err) } } enc.Flush() return nil } +var xmlCharRx = regexp.MustCompile("[<>&]") + +// escapeXMLText is used by generated code to escape text in XML, but only +// escaping the characters `<`, `>`, and `&`. +// +// This is provided in order to work around SOAP server implementations that +// fail to decode XML correctly, specifically failing to decode `"`, `'`. Note +// that this can only be safely used for injecting into XML text, but not into +// attributes or other contexts. +func escapeXMLText(s string) string { + return xmlCharRx.ReplaceAllStringFunc(s, replaceEntity) +} + +func replaceEntity(s string) string { + switch s { + case "<": + return "<" + case ">": + return ">" + case "&": + return "&" + } + return s +} + 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 index 75dbbdb..889a009 100644 --- a/soap/soap_test.go +++ b/soap/soap_test.go @@ -21,6 +21,7 @@ func (rt *capturingRoundTripper) RoundTrip(req *http.Request) (*http.Response, e } func TestActionInputs(t *testing.T) { + t.Parallel() url, err := url.Parse("http://example.com/soap") if err != nil { t.Fatal(err) @@ -51,12 +52,13 @@ func TestActionInputs(t *testing.T) { type In struct { Foo string Bar string `soap:"bar"` + Baz string } type Out struct { A string B string } - in := In{"foo", "bar"} + in := In{"foo", "bar", "quoted=\"baz\""} gotOut := Out{} err = client.PerformAction("mynamespace", "myaction", &in, &gotOut) if err != nil { @@ -67,6 +69,7 @@ func TestActionInputs(t *testing.T) { `` + `foo` + `bar` + + `quoted="baz"` + `` + soapSuffix) body, err := ioutil.ReadAll(rt.capturedReq.Body) @@ -83,3 +86,26 @@ func TestActionInputs(t *testing.T) { t.Errorf("Bad output\nwant: %+v\n got: %+v", wantOut, gotOut) } } + + +func TestEscapeXMLText(t *testing.T) { + t.Parallel() + tests := []struct { + input string + want string + }{ + {"", ""}, + {"abc123", "abc123"}, + {"&", "<foo>&"}, + {"\"foo'", "\"foo'"}, + } + for _, test := range tests { + test := test + t.Run(test.input, func(t *testing.T) { + got := escapeXMLText(test.input) + if got != test.want { + t.Errorf("want %q, got %q", test.want, got) + } + }) + } +}