soap.go 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
  1. // Definition for the SOAP structure required for UPnP's SOAP usage.
  2. package soap
  3. import (
  4. "bytes"
  5. "context"
  6. "encoding/xml"
  7. "fmt"
  8. "io/ioutil"
  9. "net/http"
  10. "net/url"
  11. "reflect"
  12. "strings"
  13. )
  14. const (
  15. soapEncodingStyle = "http://schemas.xmlsoap.org/soap/encoding/"
  16. soapPrefix = xml.Header + `<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><s:Body>`
  17. soapSuffix = `</s:Body></s:Envelope>`
  18. )
  19. type SOAPClient struct {
  20. EndpointURL url.URL
  21. HTTPClient *http.Client
  22. }
  23. // NewSoapClient creates a new soap client to a specific URL using a given http.Client.
  24. // The http.Client must not be nil.
  25. func NewSOAPClient(endpointURL url.URL, httpClient *http.Client) *SOAPClient {
  26. return &SOAPClient{
  27. EndpointURL: endpointURL,
  28. HTTPClient: httpClient,
  29. }
  30. }
  31. // PerformSOAPAction makes a SOAP request, with the given action.
  32. // inAction and outAction must both be pointers to structs with string fields
  33. // only.
  34. func (client *SOAPClient) PerformAction(
  35. ctx context.Context, actionNamespace, actionName string,
  36. inAction interface{}, outAction interface{},
  37. ) error {
  38. requestBytes, err := encodeRequestAction(actionNamespace, actionName, inAction)
  39. if err != nil {
  40. return err
  41. }
  42. req := &http.Request{
  43. Method: "POST",
  44. URL: &client.EndpointURL,
  45. Header: http.Header{
  46. "SOAPACTION": []string{`"` + actionNamespace + "#" + actionName + `"`},
  47. "CONTENT-TYPE": []string{"text/xml; charset=\"utf-8\""},
  48. },
  49. Body: ioutil.NopCloser(bytes.NewBuffer(requestBytes)),
  50. // Set ContentLength to avoid chunked encoding - some servers might not support it.
  51. ContentLength: int64(len(requestBytes)),
  52. }
  53. response, err := client.HTTPClient.Do(req.WithContext(ctx))
  54. if err != nil {
  55. return fmt.Errorf("goupnp: error performing SOAP HTTP request: %v", err)
  56. }
  57. defer response.Body.Close()
  58. if response.StatusCode != 200 && response.ContentLength == 0 {
  59. return fmt.Errorf("goupnp: SOAP request got HTTP %s", response.Status)
  60. }
  61. responseEnv := newSOAPEnvelope()
  62. decoder := xml.NewDecoder(response.Body)
  63. if err := decoder.Decode(responseEnv); err != nil {
  64. return fmt.Errorf("goupnp: error decoding response body: %v", err)
  65. }
  66. if responseEnv.Body.Fault != nil {
  67. return responseEnv.Body.Fault
  68. } else if response.StatusCode != 200 {
  69. return fmt.Errorf("goupnp: SOAP request got HTTP %s", response.Status)
  70. }
  71. if outAction == nil {
  72. return nil
  73. }
  74. if err := xml.Unmarshal(responseEnv.Body.RawAction, outAction); err != nil {
  75. return fmt.Errorf("goupnp: error unmarshalling out action: %v, %v", err, responseEnv.Body.RawAction)
  76. }
  77. return nil
  78. }
  79. // newSOAPAction creates a soapEnvelope with the given action and arguments.
  80. func newSOAPEnvelope() *soapEnvelope {
  81. return &soapEnvelope{EncodingStyle: soapEncodingStyle}
  82. }
  83. // encodeRequestAction is a hacky way to create an encoded SOAP envelope
  84. // containing the given action. Experiments with one router have shown that it
  85. // 500s for requests where the outer default xmlns is set to the SOAP
  86. // namespace, and then reassigning the default namespace within that to the
  87. // service namespace. Hand-coding the outer XML to work-around this.
  88. func encodeRequestAction(actionNamespace, actionName string, inAction interface{}) ([]byte, error) {
  89. requestBuf := new(bytes.Buffer)
  90. requestBuf.WriteString(soapPrefix)
  91. requestBuf.WriteString(`<u:`)
  92. xml.EscapeText(requestBuf, []byte(actionName))
  93. requestBuf.WriteString(` xmlns:u="`)
  94. xml.EscapeText(requestBuf, []byte(actionNamespace))
  95. requestBuf.WriteString(`">`)
  96. if inAction != nil {
  97. if err := encodeRequestArgs(requestBuf, inAction); err != nil {
  98. return nil, err
  99. }
  100. }
  101. requestBuf.WriteString(`</u:`)
  102. xml.EscapeText(requestBuf, []byte(actionName))
  103. requestBuf.WriteString(`>`)
  104. requestBuf.WriteString(soapSuffix)
  105. return requestBuf.Bytes(), nil
  106. }
  107. func encodeRequestArgs(w *bytes.Buffer, inAction interface{}) error {
  108. in := reflect.Indirect(reflect.ValueOf(inAction))
  109. if in.Kind() != reflect.Struct {
  110. return fmt.Errorf("goupnp: SOAP inAction is not a struct but of type %v", in.Type())
  111. }
  112. enc := xml.NewEncoder(w)
  113. nFields := in.NumField()
  114. inType := in.Type()
  115. for i := 0; i < nFields; i++ {
  116. field := inType.Field(i)
  117. argName := field.Name
  118. if nameOverride := field.Tag.Get("soap"); nameOverride != "" {
  119. argName = nameOverride
  120. }
  121. value := in.Field(i)
  122. if value.Kind() != reflect.String {
  123. return fmt.Errorf("goupnp: SOAP arg %q is not of type string, but of type %v", argName, value.Type())
  124. }
  125. elem := xml.StartElement{Name: xml.Name{Local: argName}}
  126. if err := enc.EncodeToken(elem); err != nil {
  127. return fmt.Errorf("goupnp: error encoding start element for SOAP arg %q: %v", argName, err)
  128. }
  129. if err := enc.Flush(); err != nil {
  130. return fmt.Errorf("goupnp: error flushing start element for SOAP arg %q: %v", argName, err)
  131. }
  132. if _, err := w.Write([]byte(escapeXMLText(value.Interface().(string)))); err != nil {
  133. return fmt.Errorf("goupnp: error writing value for SOAP arg %q: %v", argName, err)
  134. }
  135. if err := enc.EncodeToken(elem.End()); err != nil {
  136. return fmt.Errorf("goupnp: error encoding end element for SOAP arg %q: %v", argName, err)
  137. }
  138. }
  139. enc.Flush()
  140. return nil
  141. }
  142. var replacer = strings.NewReplacer("<", "&lt;", ">", "&gt;", "&", "&amp;")
  143. // escapeXMLText is used by generated code to escape text in XML, but only
  144. // escaping the characters `<`, `>`, and `&`.
  145. //
  146. // This is provided in order to work around SOAP server implementations that
  147. // fail to decode XML correctly, specifically failing to decode `"`, `'`. Note
  148. // that this can only be safely used for injecting into XML text, but not into
  149. // attributes or other contexts.
  150. func escapeXMLText(s string) string {
  151. return replacer.Replace(s)
  152. }
  153. type soapEnvelope struct {
  154. XMLName xml.Name `xml:"http://schemas.xmlsoap.org/soap/envelope/ Envelope"`
  155. EncodingStyle string `xml:"http://schemas.xmlsoap.org/soap/envelope/ encodingStyle,attr"`
  156. Body soapBody `xml:"http://schemas.xmlsoap.org/soap/envelope/ Body"`
  157. }
  158. type soapBody struct {
  159. Fault *SOAPFaultError `xml:"Fault"`
  160. RawAction []byte `xml:",innerxml"`
  161. }
  162. // SOAPFaultError implements error, and contains SOAP fault information.
  163. type SOAPFaultError struct {
  164. FaultCode string `xml:"faultCode"`
  165. FaultString string `xml:"faultString"`
  166. Detail struct {
  167. Raw []byte `xml:",innerxml"`
  168. } `xml:"detail"`
  169. }
  170. func (err *SOAPFaultError) Error() string {
  171. return fmt.Sprintf("SOAP fault: %s", err.FaultString)
  172. }