From f8d565399b079b57ba0ac5a6dbdfae86c5ecb81d Mon Sep 17 00:00:00 2001 From: John Beisley Date: Sat, 26 Mar 2022 18:14:59 +0000 Subject: [PATCH 1/3] Ignore experiments under v2alpha/cmd. --- v2alpha/.gitignore | 1 + 1 file changed, 1 insertion(+) create mode 100644 v2alpha/.gitignore diff --git a/v2alpha/.gitignore b/v2alpha/.gitignore new file mode 100644 index 0000000..2db6493 --- /dev/null +++ b/v2alpha/.gitignore @@ -0,0 +1 @@ +cmd/*experiment From d4fdaef967b76213e58ba998e88d96a808036858 Mon Sep 17 00:00:00 2001 From: John Beisley Date: Sat, 26 Mar 2022 18:17:06 +0000 Subject: [PATCH 2/3] Fix how envelopes are marshalled and unmarshalled. Previously the arguments would be wrapped an additional XML element. --- v2alpha/soap/envelope/envelope.go | 95 ++++++++++++-------------- v2alpha/soap/envelope/envelope_test.go | 65 +++++++++++++----- 2 files changed, 93 insertions(+), 67 deletions(-) diff --git a/v2alpha/soap/envelope/envelope.go b/v2alpha/soap/envelope/envelope.go index 7763b56..9d691b8 100644 --- a/v2alpha/soap/envelope/envelope.go +++ b/v2alpha/soap/envelope/envelope.go @@ -32,17 +32,6 @@ func (fe *Fault) Is(target error) bool { return target == ErrFault } -// Various "constant" bytes used in the written envelope. -var ( - envOpen = []byte(xml.Header + ``) - env1 = []byte(``) - env4 = []byte(``) - envClose = []byte(``) -) - // Action wraps a SOAP action to be read or written as part of a SOAP envelope. type Action struct { // XMLName specifies the XML element namespace (URI) and name. Together @@ -52,18 +41,57 @@ type Action struct { // arguments. See https://pkg.go.dev/encoding/xml@go1.17.1#Marshal and // https://pkg.go.dev/encoding/xml@go1.17.1#Unmarshal for details on // annotating fields in the structure. - Args any `xml:",any"` + Args any } -// NewAction creates a SOAP action for sending with the given namespace URL, +// NewSendAction creates a SOAP action for receiving arguments. +func NewRecvAction(args any) *Action { + return &Action{Args: args} +} + +// NewSendAction creates a SOAP action for sending with the given namespace URL, // action name, and arguments. -func NewAction(nsURL, actionName string, args any) *Action { +func NewSendAction(serviceType, actionName string, args any) *Action { return &Action{ - XMLName: xml.Name{Space: nsURL, Local: actionName}, + XMLName: xml.Name{Space: serviceType, Local: actionName}, Args: args, } } +var _ xml.Marshaler = &Action{} + +// MarshalXML implements `xml.Marshaller`. +// +// This is an implementation detail that allows packing elements inside the +// action element from the struct in `a.Args`. +func (a *Action) MarshalXML(e *xml.Encoder, start xml.StartElement) error { + // Hardcodes the XML namespace. See comment in Write() for context. + return e.EncodeElement(a.Args, xml.StartElement{ + Name: xml.Name{Space: "", Local: "u:" + a.XMLName.Local}, + Attr: []xml.Attr{{ + Name: xml.Name{Space: "", Local: "xmlns:u"}, + Value: a.XMLName.Space, + }}, + }) +} + +var _ xml.Unmarshaler = &Action{} + +// UnmarshalXML implements `xml.Unmarshaller`. +// +// This is an implementation detail that allows unpacking elements inside the +// action element into the struct in `a.Args`. +func (a *Action) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { + a.XMLName = start.Name + return d.DecodeElement(a.Args, &start) +} + +// Various "constant" bytes used in the written envelope. +var ( + envOpen = []byte(xml.Header + ``) + envClose = []byte(``) +) + // Write marshals a SOAP envelope to the writer. Errors can be from the writer // or XML encoding. func Write(w io.Writer, action *Action) error { @@ -79,47 +107,12 @@ func Write(w io.Writer, action *Action) error { if err != nil { return err } - _, err = w.Write(env1) - if err != nil { - return err - } - err = xml.EscapeText(w, []byte(action.XMLName.Local)) - if err != nil { - return err - } - _, err = w.Write(env2) - if err != nil { - return err - } - err = xml.EscapeText(w, []byte(action.XMLName.Space)) - if err != nil { - return err - } - _, err = w.Write(env3) - if err != nil { - return err - } enc := xml.NewEncoder(w) - err = enc.Encode(action.Args) + err = enc.Encode(action) if err != nil { return err } err = enc.Flush() - if err != nil { - return err - } - _, err = w.Write(env4) - if err != nil { - return err - } - xml.EscapeText(w, []byte(action.XMLName.Local)) - if err != nil { - return err - } - _, err = w.Write(env5) - if err != nil { - return err - } _, err = w.Write(envClose) return err } diff --git a/v2alpha/soap/envelope/envelope_test.go b/v2alpha/soap/envelope/envelope_test.go index 2198c3f..c90dc61 100644 --- a/v2alpha/soap/envelope/envelope_test.go +++ b/v2alpha/soap/envelope/envelope_test.go @@ -8,34 +8,69 @@ import ( "io" "reflect" "testing" + + "github.com/google/go-cmp/cmp" ) -func TestWriteRead(t *testing.T) { - type Args struct { - Foo string `xml:"foo"` - Bar string `xml:"bar"` - } +type testArgs struct { + Foo string + Bar string +} - sendAction := NewAction("http://example.com/namespace", "MyAction", &Args{ +// TestWriteRead tests the round-trip of writing an envelope and reading it back. +func TestWriteRead(t *testing.T) { + argsIn := &testArgs{ Foo: "foo-1", Bar: "bar-2", - }) + } + actionIn := NewSendAction("urn:schemas-upnp-org:service:FakeService:1", "MyAction", argsIn) buf := &bytes.Buffer{} - err := Write(buf, sendAction) + err := Write(buf, actionIn) if err != nil { - t.Errorf("Write want success, got err=%v", err) + t.Fatalf("Write want success, got err=%v", err) } + t.Logf("Encoded envelope:\n%v", buf) - recvAction := &Action{Args: &Args{}} + argsOut := &testArgs{} + actionOut := NewRecvAction(argsOut) - err = Read(buf, recvAction) + err = Read(buf, actionOut) if err != nil { t.Errorf("Read want success, got err=%v", err) } - if !reflect.DeepEqual(sendAction, recvAction) { - t.Errorf("want recvAction=%+v, got %+v", sendAction, recvAction) + if diff := cmp.Diff(actionIn, actionOut); diff != "" { + t.Errorf("\nwant actionOut=%+v\ngot %+v\ndiff:\n%s", actionIn, actionOut, diff) + } + if diff := cmp.Diff(argsIn, argsOut); diff != "" { + t.Errorf("\nwant argsOut=%+v\ngot %+v\ndiff:\n%s", argsIn, argsOut, diff) + } +} + +// TestRead tests read against a semi-real encoded envelope. +func TestRead(t *testing.T) { + env := []byte(` + +foo-1 +bar-2 + + `) + argsOut := &testArgs{} + + actionOut := NewRecvAction(argsOut) + + if err := Read(bytes.NewBuffer(env), actionOut); err != nil { + t.Fatalf("Read want success, got err=%v", err) + } + + wantArgsOut := &testArgs{ + Foo: "foo-1", + Bar: "bar-2", + } + + if diff := cmp.Diff(wantArgsOut, argsOut); diff != "" { + t.Errorf("want argsOut=%+v, got %+v\ndiff:\n%s", wantArgsOut, argsOut, diff) } } @@ -54,9 +89,7 @@ s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"> `) - type args struct{} - - err := Read(bytes.NewBuffer(env), &Action{Args: &args{}}) + err := Read(bytes.NewBuffer(env), NewRecvAction(&testArgs{})) if err == nil { t.Fatal("want err != nil, got nil") } From 767078168a50c656a4f33c6ca42cb71e5a2e4660 Mon Sep 17 00:00:00 2001 From: John Beisley Date: Sat, 26 Mar 2022 18:17:39 +0000 Subject: [PATCH 3/3] Clean up SOAP client API. --- v2alpha/soap/client/client.go | 42 ++++++++++++++++++++++-------- v2alpha/soap/client/client_test.go | 4 +-- 2 files changed, 33 insertions(+), 13 deletions(-) diff --git a/v2alpha/soap/client/client.go b/v2alpha/soap/client/client.go index 8d4ea22..5f0be74 100644 --- a/v2alpha/soap/client/client.go +++ b/v2alpha/soap/client/client.go @@ -55,18 +55,32 @@ func New(endpointURL string, opts ...Option) *Client { } } -// PerformAction makes a SOAP request, with the given action values to provide -// arguments (`args`) and capture the `reply` into. If `client` is nil, then -// http.DefaultClient is used. +// PerformAction makes a SOAP request, with the given `argsIn` as input +// arguments, and `argsOut` to capture the output arguments into. +// `serviceType` is the SOAP service type URN, `actionName` is the action to +// call. +// +// This is a convenience for calling `c.Do` without creating `*Action` values. func (c *Client) PerformAction( + ctx context.Context, serviceType, actionName string, + argsIn, argsOut any, +) error { + actionIn := envelope.NewSendAction(serviceType, actionName, argsIn) + actionOut := &envelope.Action{Args: argsOut} + return c.Do(ctx, actionIn, actionOut) +} + +// PerformAction makes a SOAP request, with the given action values to provide +// arguments (`args`) and capture the `reply` into. +func (c *Client) Do( ctx context.Context, - args, reply *envelope.Action, + actionIn, actionOut *envelope.Action, ) error { req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.endpointURL, nil) if err != nil { return err } - if err := SetRequestAction(req, args); err != nil { + if err := SetRequestAction(req, actionIn); err != nil { return err } @@ -81,15 +95,18 @@ func (c *Client) PerformAction( resp.Status, resp.StatusCode) } - return ParseResponseAction(resp, reply) + return ParseResponseAction(resp, actionOut) } // SetRequestAction updates fields in `req` with the given SOAP action. // Specifically it sets Body, ContentLength, Method, and the SOAPACTION and // CONTENT-TYPE headers. -func SetRequestAction(req *http.Request, args *envelope.Action) error { +func SetRequestAction( + req *http.Request, + actionIn *envelope.Action, +) error { buf := &bytes.Buffer{} - err := envelope.Write(buf, args) + err := envelope.Write(buf, actionIn) if err != nil { return fmt.Errorf("encoding envelope: %w", err) } @@ -98,7 +115,7 @@ func SetRequestAction(req *http.Request, args *envelope.Action) error { req.ContentLength = int64(buf.Len()) req.Method = http.MethodPost req.Header["SOAPACTION"] = []string{fmt.Sprintf( - `"%s#%s"`, args.XMLName.Space, args.XMLName.Local)} + `"%s#%s"`, actionIn.XMLName.Space, actionIn.XMLName.Local)} req.Header["CONTENT-TYPE"] = []string{`text/xml; charset="utf-8"`} return nil @@ -107,7 +124,10 @@ func SetRequestAction(req *http.Request, args *envelope.Action) error { // ParseResponse extracts a parsed action from an HTTP response. // The caller is responsible for calling resp.Body.Close(), but this function // will consume the entire response body. -func ParseResponseAction(resp *http.Response, reply *envelope.Action) error { +func ParseResponseAction( + resp *http.Response, + actionOut *envelope.Action, +) error { if resp.Body == nil { return errors.New("missing response body") } @@ -117,7 +137,7 @@ func ParseResponseAction(resp *http.Response, reply *envelope.Action) error { return fmt.Errorf("reading response body: %w", err) } - if err := envelope.Read(buf, reply); err != nil { + if err := envelope.Read(buf, actionOut); err != nil { if _, ok := err.(*envelope.Fault); ok { // Parsed cleanly, got SOAP fault. return err diff --git a/v2alpha/soap/client/client_test.go b/v2alpha/soap/client/client_test.go index 69a0a2b..1810c80 100644 --- a/v2alpha/soap/client/client_test.go +++ b/v2alpha/soap/client/client_test.go @@ -76,7 +76,7 @@ func (fss *fakeSoapServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { } } -func TestPerformAction(t *testing.T) { +func TestDo(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) t.Cleanup(cancel) @@ -101,7 +101,7 @@ func TestPerformAction(t *testing.T) { reply := &ActionReply{} replyAction := &envelope.Action{Args: reply} - if err := c.PerformAction(ctx, reqAction, replyAction); err != nil { + if err := c.Do(ctx, reqAction, replyAction); err != nil { t.Errorf("got error: %v, want success", err) } else { if got, want := reply.Greeting, "Hello, World!"; got != want {