goupnp/soap/soap.go

212 lines
6.9 KiB
Go
Raw Normal View History

2013-09-29 10:23:10 +00:00
// Definition for the SOAP structure required for UPnP's SOAP usage.
package soap
2013-09-29 10:23:10 +00:00
import (
"bytes"
2021-07-11 13:15:35 +00:00
"context"
2013-09-29 10:23:10 +00:00
"encoding/xml"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"reflect"
"regexp"
2013-09-29 10:23:10 +00:00
)
const (
2013-10-06 11:57:26 +00:00
soapEncodingStyle = "http://schemas.xmlsoap.org/soap/encoding/"
soapPrefix = xml.Header + `<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><s:Body>`
2013-10-06 11:57:26 +00:00
soapSuffix = `</s:Body></s:Envelope>`
2013-09-29 10:23:10 +00:00
)
2013-10-06 11:57:26 +00:00
type SOAPClient struct {
EndpointURL url.URL
HTTPClient http.Client
2013-09-29 10:23:10 +00:00
}
2013-10-06 11:57:26 +00:00
func NewSOAPClient(endpointURL url.URL) *SOAPClient {
return &SOAPClient{
EndpointURL: endpointURL,
2013-09-29 10:23:10 +00:00
}
}
2013-10-06 11:57:26 +00:00
// PerformSOAPAction makes a SOAP request, with the given action.
// inAction and outAction must both be pointers to structs with string fields
// only.
2021-07-11 13:15:35 +00:00
func (client *SOAPClient) PerformActionCtx(ctx context.Context, actionNamespace, actionName string, inAction interface{}, outAction interface{}) error {
requestBytes, err := encodeRequestAction(actionNamespace, actionName, inAction)
if err != nil {
return err
}
2013-09-29 10:23:10 +00:00
2021-07-11 13:15:35 +00:00
req := &http.Request{
2013-09-29 10:23:10 +00:00
Method: "POST",
2013-10-06 11:57:26 +00:00
URL: &client.EndpointURL,
2013-09-29 10:23:10 +00:00
Header: http.Header{
"SOAPACTION": []string{`"` + actionNamespace + "#" + actionName + `"`},
2013-09-29 10:23:10 +00:00
"CONTENT-TYPE": []string{"text/xml; charset=\"utf-8\""},
},
Body: ioutil.NopCloser(bytes.NewBuffer(requestBytes)),
// Set ContentLength to avoid chunked encoding - some servers might not support it.
ContentLength: int64(len(requestBytes)),
2021-07-11 13:15:35 +00:00
}
req = req.WithContext(ctx)
response, err := client.HTTPClient.Do(req)
2013-09-29 10:23:10 +00:00
if err != nil {
return fmt.Errorf("goupnp: error performing SOAP HTTP request: %v", err)
2013-09-29 10:23:10 +00:00
}
defer response.Body.Close()
if response.StatusCode != 200 && response.ContentLength == 0 {
2013-10-06 11:57:26 +00:00
return fmt.Errorf("goupnp: SOAP request got HTTP %s", response.Status)
2013-09-29 10:23:10 +00:00
}
2013-10-06 11:57:26 +00:00
responseEnv := newSOAPEnvelope()
2013-09-29 10:23:10 +00:00
decoder := xml.NewDecoder(response.Body)
2013-10-06 11:57:26 +00:00
if err := decoder.Decode(responseEnv); err != nil {
return fmt.Errorf("goupnp: error decoding response body: %v", err)
2013-09-29 10:23:10 +00:00
}
if responseEnv.Body.Fault != nil {
2013-10-06 11:57:26 +00:00
return responseEnv.Body.Fault
} else if response.StatusCode != 200 {
return fmt.Errorf("goupnp: SOAP request got HTTP %s", response.Status)
2013-10-06 11:57:26 +00:00
}
if outAction != nil {
if err := xml.Unmarshal(responseEnv.Body.RawAction, outAction); err != nil {
return fmt.Errorf("goupnp: error unmarshalling out action: %v, %v", err, responseEnv.Body.RawAction)
}
}
2013-10-06 11:57:26 +00:00
return nil
}
2021-07-11 13:15:35 +00:00
// PerformAction is the legacy version of PerformActionCtx, which uses
// context.Background.
func (client *SOAPClient) PerformAction(actionNamespace, actionName string, inAction interface{}, outAction interface{}) error {
return client.PerformActionCtx(context.Background(), actionNamespace, actionName, inAction, outAction)
}
2013-10-06 11:57:26 +00:00
// newSOAPAction creates a soapEnvelope with the given action and arguments.
func newSOAPEnvelope() *soapEnvelope {
return &soapEnvelope{
EncodingStyle: soapEncodingStyle,
2013-09-29 10:23:10 +00:00
}
2013-10-06 11:57:26 +00:00
}
2013-09-29 10:23:10 +00:00
2013-10-06 11:57:26 +00:00
// encodeRequestAction is a hacky way to create an encoded SOAP envelope
// containing the given action. Experiments with one router have shown that it
// 500s for requests where the outer default xmlns is set to the SOAP
// namespace, and then reassigning the default namespace within that to the
// service namespace. Hand-coding the outer XML to work-around this.
func encodeRequestAction(actionNamespace, actionName string, inAction interface{}) ([]byte, error) {
2013-10-06 11:57:26 +00:00
requestBuf := new(bytes.Buffer)
requestBuf.WriteString(soapPrefix)
requestBuf.WriteString(`<u:`)
xml.EscapeText(requestBuf, []byte(actionName))
requestBuf.WriteString(` xmlns:u="`)
xml.EscapeText(requestBuf, []byte(actionNamespace))
requestBuf.WriteString(`">`)
if inAction != nil {
if err := encodeRequestArgs(requestBuf, inAction); err != nil {
return nil, err
}
2013-10-06 11:57:26 +00:00
}
requestBuf.WriteString(`</u:`)
xml.EscapeText(requestBuf, []byte(actionName))
requestBuf.WriteString(`>`)
2013-10-06 11:57:26 +00:00
requestBuf.WriteString(soapSuffix)
return requestBuf.Bytes(), nil
2013-09-29 10:23:10 +00:00
}
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())
}
2021-07-04 09:59:33 +00:00
elem := xml.StartElement{Name: xml.Name{Space: "", Local: argName}, Attr: 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 "&lt;"
case ">":
return "&gt;"
case "&":
return "&amp;"
}
return s
}
2013-10-06 11:57:26 +00:00
type soapEnvelope struct {
2013-09-29 10:23:10 +00:00
XMLName xml.Name `xml:"http://schemas.xmlsoap.org/soap/envelope/ Envelope"`
EncodingStyle string `xml:"http://schemas.xmlsoap.org/soap/envelope/ encodingStyle,attr"`
2013-10-06 11:57:26 +00:00
Body soapBody `xml:"http://schemas.xmlsoap.org/soap/envelope/ Body"`
2013-09-29 10:23:10 +00:00
}
2013-10-06 11:57:26 +00:00
type soapBody struct {
Fault *SOAPFaultError `xml:"Fault"`
RawAction []byte `xml:",innerxml"`
2013-09-29 10:23:10 +00:00
}
2013-10-06 11:57:26 +00:00
// SOAPFaultError implements error, and contains SOAP fault information.
type SOAPFaultError struct {
FaultCode string `xml:"faultcode"`
FaultString string `xml:"faultstring"`
2021-05-22 19:28:24 +00:00
Detail struct {
UPnPError struct {
Errorcode int `xml:"errorCode"`
ErrorDescription string `xml:"errorDescription"`
} `xml:"UPnPError"`
2021-05-22 19:28:24 +00:00
Raw []byte `xml:",innerxml"`
} `xml:"detail"`
}
2013-10-06 11:57:26 +00:00
func (err *SOAPFaultError) Error() string {
return fmt.Sprintf("SOAP fault. Code: %s | Explanation: %s | Detail: %s",
err.FaultCode, err.FaultString, string(err.Detail.Raw))
}