Merge branch 'main' of github.com:huin/goupnp into main
This commit is contained in:
commit
5c7f90426b
1
v2alpha/.gitignore
vendored
Normal file
1
v2alpha/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
cmd/*experiment
|
@ -57,18 +57,32 @@ func New(endpointURL string, opts ...Option) *Client {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// PerformAction makes a SOAP request, with the given action values to provide
|
// PerformAction makes a SOAP request, with the given `argsIn` as input
|
||||||
// arguments (`args`) and capture the `reply` into. If `client` is nil, then
|
// arguments, and `argsOut` to capture the output arguments into.
|
||||||
// http.DefaultClient is used.
|
// `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(
|
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,
|
ctx context.Context,
|
||||||
args, reply *envelope.Action,
|
actionIn, actionOut *envelope.Action,
|
||||||
) error {
|
) error {
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.endpointURL, nil)
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.endpointURL, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := SetRequestAction(req, args); err != nil {
|
if err := SetRequestAction(req, actionIn); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -83,15 +97,18 @@ func (c *Client) PerformAction(
|
|||||||
resp.Status, resp.StatusCode)
|
resp.Status, resp.StatusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
return ParseResponseAction(resp, reply)
|
return ParseResponseAction(resp, actionOut)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetRequestAction updates fields in `req` with the given SOAP action.
|
// SetRequestAction updates fields in `req` with the given SOAP action.
|
||||||
// Specifically it sets Body, ContentLength, Method, and the SOAPACTION and
|
// Specifically it sets Body, ContentLength, Method, and the SOAPACTION and
|
||||||
// CONTENT-TYPE headers.
|
// CONTENT-TYPE headers.
|
||||||
func SetRequestAction(req *http.Request, args *envelope.Action) error {
|
func SetRequestAction(
|
||||||
|
req *http.Request,
|
||||||
|
actionIn *envelope.Action,
|
||||||
|
) error {
|
||||||
buf := &bytes.Buffer{}
|
buf := &bytes.Buffer{}
|
||||||
err := envelope.Write(buf, args)
|
err := envelope.Write(buf, actionIn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("encoding envelope: %w", err)
|
return fmt.Errorf("encoding envelope: %w", err)
|
||||||
}
|
}
|
||||||
@ -100,7 +117,7 @@ func SetRequestAction(req *http.Request, args *envelope.Action) error {
|
|||||||
req.ContentLength = int64(buf.Len())
|
req.ContentLength = int64(buf.Len())
|
||||||
req.Method = http.MethodPost
|
req.Method = http.MethodPost
|
||||||
req.Header["SOAPACTION"] = []string{fmt.Sprintf(
|
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"`}
|
req.Header["CONTENT-TYPE"] = []string{`text/xml; charset="utf-8"`}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@ -109,7 +126,10 @@ func SetRequestAction(req *http.Request, args *envelope.Action) error {
|
|||||||
// ParseResponse extracts a parsed action from an HTTP response.
|
// ParseResponse extracts a parsed action from an HTTP response.
|
||||||
// The caller is responsible for calling resp.Body.Close(), but this function
|
// The caller is responsible for calling resp.Body.Close(), but this function
|
||||||
// will consume the entire response body.
|
// 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 {
|
if resp.Body == nil {
|
||||||
return errors.New("missing response body")
|
return errors.New("missing response body")
|
||||||
}
|
}
|
||||||
@ -119,7 +139,7 @@ func ParseResponseAction(resp *http.Response, reply *envelope.Action) error {
|
|||||||
return fmt.Errorf("reading response body: %w", err)
|
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 {
|
if _, ok := err.(*envelope.Fault); ok {
|
||||||
// Parsed cleanly, got SOAP fault.
|
// Parsed cleanly, got SOAP fault.
|
||||||
return err
|
return err
|
||||||
|
@ -75,7 +75,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)
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
t.Cleanup(cancel)
|
t.Cleanup(cancel)
|
||||||
|
|
||||||
@ -96,7 +96,7 @@ func TestPerformAction(t *testing.T) {
|
|||||||
reply := &ActionReply{}
|
reply := &ActionReply{}
|
||||||
replyAction := &envelope.Action{Args: reply}
|
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)
|
t.Errorf("got error: %v, want success", err)
|
||||||
} else {
|
} else {
|
||||||
if got, want := reply.Greeting, "Hello, World!"; got != want {
|
if got, want := reply.Greeting, "Hello, World!"; got != want {
|
||||||
|
@ -32,17 +32,6 @@ func (fe *Fault) Is(target error) bool {
|
|||||||
return target == ErrFault
|
return target == ErrFault
|
||||||
}
|
}
|
||||||
|
|
||||||
// Various "constant" bytes used in the written envelope.
|
|
||||||
var (
|
|
||||||
envOpen = []byte(xml.Header + `<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><s:Body>`)
|
|
||||||
env1 = []byte(`<u:`)
|
|
||||||
env2 = []byte(` xmlns:u="`)
|
|
||||||
env3 = []byte(`">`)
|
|
||||||
env4 = []byte(`</u:`)
|
|
||||||
env5 = []byte(`>`)
|
|
||||||
envClose = []byte(`</s:Body></s:Envelope>`)
|
|
||||||
)
|
|
||||||
|
|
||||||
// Action wraps a SOAP action to be read or written as part of a SOAP envelope.
|
// Action wraps a SOAP action to be read or written as part of a SOAP envelope.
|
||||||
type Action struct {
|
type Action struct {
|
||||||
// XMLName specifies the XML element namespace (URI) and name. Together
|
// 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
|
// 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
|
// https://pkg.go.dev/encoding/xml@go1.17.1#Unmarshal for details on
|
||||||
// annotating fields in the structure.
|
// 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.
|
// action name, and arguments.
|
||||||
func NewAction(nsURL, actionName string, args any) *Action {
|
func NewSendAction(serviceType, actionName string, args any) *Action {
|
||||||
return &Action{
|
return &Action{
|
||||||
XMLName: xml.Name{Space: nsURL, Local: actionName},
|
XMLName: xml.Name{Space: serviceType, Local: actionName},
|
||||||
Args: args,
|
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 + `<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><s:Body>`)
|
||||||
|
envClose = []byte(`</s:Body></s:Envelope>`)
|
||||||
|
)
|
||||||
|
|
||||||
// Write marshals a SOAP envelope to the writer. Errors can be from the writer
|
// Write marshals a SOAP envelope to the writer. Errors can be from the writer
|
||||||
// or XML encoding.
|
// or XML encoding.
|
||||||
func Write(w io.Writer, action *Action) error {
|
func Write(w io.Writer, action *Action) error {
|
||||||
@ -79,47 +107,12 @@ func Write(w io.Writer, action *Action) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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)
|
enc := xml.NewEncoder(w)
|
||||||
err = enc.Encode(action.Args)
|
err = enc.Encode(action)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = enc.Flush()
|
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)
|
_, err = w.Write(envClose)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -8,34 +8,69 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestWriteRead(t *testing.T) {
|
type testArgs struct {
|
||||||
type Args struct {
|
Foo string
|
||||||
Foo string `xml:"foo"`
|
Bar string
|
||||||
Bar string `xml:"bar"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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",
|
Foo: "foo-1",
|
||||||
Bar: "bar-2",
|
Bar: "bar-2",
|
||||||
})
|
}
|
||||||
|
actionIn := NewSendAction("urn:schemas-upnp-org:service:FakeService:1", "MyAction", argsIn)
|
||||||
|
|
||||||
buf := &bytes.Buffer{}
|
buf := &bytes.Buffer{}
|
||||||
err := Write(buf, sendAction)
|
err := Write(buf, actionIn)
|
||||||
if err != nil {
|
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 {
|
if err != nil {
|
||||||
t.Errorf("Read want success, got err=%v", err)
|
t.Errorf("Read want success, got err=%v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !reflect.DeepEqual(sendAction, recvAction) {
|
if diff := cmp.Diff(actionIn, actionOut); diff != "" {
|
||||||
t.Errorf("want recvAction=%+v, got %+v", sendAction, recvAction)
|
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(`<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><s:Body>
|
||||||
|
<u:FakeAction xmlns:u="urn:schemas-upnp-org:service:FakeService:1">
|
||||||
|
<Foo>foo-1</Foo>
|
||||||
|
<Bar>bar-2</Bar>
|
||||||
|
</u:FakeAction>
|
||||||
|
</s:Body> </s:Envelope>`)
|
||||||
|
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/">
|
|||||||
</s:Envelope>
|
</s:Envelope>
|
||||||
`)
|
`)
|
||||||
|
|
||||||
type args struct{}
|
err := Read(bytes.NewBuffer(env), NewRecvAction(&testArgs{}))
|
||||||
|
|
||||||
err := Read(bytes.NewBuffer(env), &Action{Args: &args{}})
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("want err != nil, got nil")
|
t.Fatal("want err != nil, got nil")
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user