soap.go 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418
  1. package soap
  2. import (
  3. "bytes"
  4. "context"
  5. "crypto/sha1"
  6. "crypto/tls"
  7. "encoding/base64"
  8. "encoding/xml"
  9. "math/rand"
  10. "net"
  11. "net/http"
  12. "strings"
  13. "time"
  14. )
  15. type SOAPEncoder interface {
  16. Encode(v interface{}) error
  17. Flush() error
  18. }
  19. type SOAPDecoder interface {
  20. Decode(v interface{}) error
  21. }
  22. type SOAPHeader struct {
  23. XMLName xml.Name `xml:"http://www.w3.org/2003/05/soap-envelope Header"`
  24. Headers []interface{} `xml:"http://www.w3.org/2003/05/soap-envelope Header"`
  25. }
  26. type SOAPEnvelope struct {
  27. XMLName xml.Name `xml:"http://www.w3.org/2003/05/soap-envelope Envelope"`
  28. Header SOAPHeader
  29. Body SOAPBody
  30. }
  31. type SOAPBody struct {
  32. XMLName xml.Name `xml:"http://www.w3.org/2003/05/soap-envelope Body"`
  33. Fault *SOAPFault `xml:",omitempty"`
  34. Content interface{} `xml:",omitempty"`
  35. }
  36. // UnmarshalXML unmarshals SOAPBody xml
  37. func (b *SOAPBody) UnmarshalXML(d *Decoder, _ StartElement) error {
  38. if b.Content == nil {
  39. return xml.UnmarshalError("Content must be a pointer to a struct")
  40. }
  41. var (
  42. token xml.Token
  43. err error
  44. consumed bool
  45. )
  46. Loop:
  47. for {
  48. if token, err = d.Token(); err != nil {
  49. return err
  50. }
  51. if token == nil {
  52. break
  53. }
  54. switch se := token.(type) {
  55. case StartElement:
  56. if consumed {
  57. return xml.UnmarshalError("Found multiple elements inside SOAP body; not wrapped-document/literal WS-I compliant")
  58. } else if se.Name.Space == "http://www.w3.org/2003/05/soap-envelope" && se.Name.Local == "Fault" {
  59. b.Fault = &SOAPFault{}
  60. b.Content = nil
  61. err = d.DecodeElement(b.Fault, &se)
  62. if err != nil {
  63. return err
  64. }
  65. consumed = true
  66. } else {
  67. if err = d.DecodeElement(b.Content, &se); err != nil {
  68. return err
  69. }
  70. consumed = true
  71. }
  72. case EndElement:
  73. break Loop
  74. }
  75. }
  76. return nil
  77. }
  78. type SOAPFaultSubCode struct {
  79. XMLName xml.Name `xml:"http://www.w3.org/2003/05/soap-envelope Subcode"`
  80. Value string
  81. }
  82. type SOAPFaultCode struct {
  83. XMLName xml.Name `xml:"http://www.w3.org/2003/05/soap-envelope Code"`
  84. Value string
  85. Subcode SOAPFaultSubCode
  86. }
  87. type SOAPFaultReason struct {
  88. XMLName xml.Name `xml:"http://www.w3.org/2003/05/soap-envelope Reason"`
  89. Text string
  90. }
  91. type SOAPFaultDetail struct {
  92. XMLName xml.Name `xml:"http://www.w3.org/2003/05/soap-envelope Detail"`
  93. Text string
  94. }
  95. type SOAPFault struct {
  96. XMLName xml.Name `xml:"http://www.w3.org/2003/05/soap-envelope Fault"`
  97. Code SOAPFaultCode
  98. Reason SOAPFaultReason `xml:",omitempty"`
  99. Detail SOAPFaultDetail `xml:",omitempty"`
  100. }
  101. func (f *SOAPFault) Error() string {
  102. s := f.Reason.Text
  103. if f.Detail.Text != "" {
  104. s += ". Details: " + f.Detail.Text
  105. }
  106. if s == "" {
  107. if f.Code.Value != "" {
  108. s = f.Code.Value + ". "
  109. }
  110. if f.Code.Subcode.Value != "" {
  111. s += f.Code.Subcode.Value
  112. }
  113. }
  114. return s
  115. }
  116. const (
  117. // Predefined WSS namespaces to be used in
  118. WssNsWSSE string = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"
  119. WssNsWSU string = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"
  120. WssNsType string = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText"
  121. mtomContentType string = `multipart/related; start-info="application/soap+xml"; type="application/xop+xml"; boundary="%s"`
  122. )
  123. type WSSPassword struct {
  124. Type string `xml:",attr"`
  125. Value string `xml:",chardata"`
  126. }
  127. type WSSNonce struct {
  128. EncodingType string `xml:",attr"`
  129. Value string `xml:",chardata"`
  130. }
  131. type WSSCreated struct {
  132. XMLName xml.Name `xml:"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd Created"`
  133. Value string `xml:",chardata"`
  134. }
  135. type WSSUsernameToken struct {
  136. Username string
  137. Password WSSPassword
  138. Nonce WSSNonce
  139. Created WSSCreated
  140. }
  141. type WSSSecurityHeader struct {
  142. XMLName xml.Name `xml:"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd Security"`
  143. MustUnderstand string `xml:"mustUnderstand,attr"`
  144. UsernameToken WSSUsernameToken
  145. }
  146. const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
  147. func randomString(n int) string {
  148. sb := strings.Builder{}
  149. sb.Grow(n)
  150. for i := 0; i < n; i++ {
  151. sb.WriteByte(charset[rand.Intn(len(charset))])
  152. }
  153. return sb.String()
  154. }
  155. // NewWSSSecurityHeader creates WSSSecurityHeader instance
  156. func NewWSSSecurityHeader(user, pass string) *WSSSecurityHeader {
  157. hdr := &WSSSecurityHeader{MustUnderstand: "1"}
  158. // Username
  159. hdr.UsernameToken.Username = user
  160. // Created
  161. hdr.UsernameToken.Created.Value = time.Now().Format("2006-01-02T15:04:05.999") + "Z"
  162. // Nonce
  163. b := make([]byte, 16)
  164. rand.Read(b)
  165. //nonce := fmt.Sprintf("%x-%x-%x-%x-%x",
  166. // b[0:4], b[4:6], b[6:8], b[8:10], b[10:])
  167. rand.Seed(time.Now().UnixNano())
  168. nonce := randomString(20)
  169. hdr.UsernameToken.Nonce.EncodingType = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary"
  170. hdr.UsernameToken.Nonce.Value = base64.StdEncoding.EncodeToString([]byte(nonce))
  171. // Password
  172. h := sha1.New()
  173. h.Write([]byte(nonce + hdr.UsernameToken.Created.Value + pass))
  174. hdr.UsernameToken.Password.Type = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest"
  175. hdr.UsernameToken.Password.Value = base64.StdEncoding.EncodeToString(h.Sum(nil))
  176. return hdr
  177. }
  178. type basicAuth struct {
  179. Login string
  180. Password string
  181. }
  182. type options struct {
  183. tlsCfg *tls.Config
  184. auth *basicAuth
  185. timeout time.Duration
  186. contimeout time.Duration
  187. tlshshaketimeout time.Duration
  188. client HTTPClient
  189. httpHeaders map[string]string
  190. }
  191. var defaultOptions = options{
  192. timeout: time.Duration(30 * time.Second),
  193. contimeout: time.Duration(90 * time.Second),
  194. tlshshaketimeout: time.Duration(15 * time.Second),
  195. }
  196. // A Option sets options such as credentials, tls, etc.
  197. type Option func(*options)
  198. // WithHTTPClient is an Option to set the HTTP client to use
  199. // This cannot be used with WithTLSHandshakeTimeout, WithTLS,
  200. // WithTimeout options
  201. func WithHTTPClient(c HTTPClient) Option {
  202. return func(o *options) {
  203. o.client = c
  204. }
  205. }
  206. // WithTLSHandshakeTimeout is an Option to set default tls handshake timeout
  207. // This option cannot be used with WithHTTPClient
  208. func WithTLSHandshakeTimeout(t time.Duration) Option {
  209. return func(o *options) {
  210. o.tlshshaketimeout = t
  211. }
  212. }
  213. // WithRequestTimeout is an Option to set default end-end connection timeout
  214. // This option cannot be used with WithHTTPClient
  215. func WithRequestTimeout(t time.Duration) Option {
  216. return func(o *options) {
  217. o.contimeout = t
  218. }
  219. }
  220. // WithBasicAuth is an Option to set BasicAuth
  221. func WithBasicAuth(login, password string) Option {
  222. return func(o *options) {
  223. o.auth = &basicAuth{Login: login, Password: password}
  224. }
  225. }
  226. // WithTLS is an Option to set tls config
  227. // This option cannot be used with WithHTTPClient
  228. func WithTLS(tls *tls.Config) Option {
  229. return func(o *options) {
  230. o.tlsCfg = tls
  231. }
  232. }
  233. // WithTimeout is an Option to set default HTTP dial timeout
  234. func WithTimeout(t time.Duration) Option {
  235. return func(o *options) {
  236. o.timeout = t
  237. }
  238. }
  239. // WithHTTPHeaders is an Option to set global HTTP headers for all requests
  240. func WithHTTPHeaders(headers map[string]string) Option {
  241. return func(o *options) {
  242. o.httpHeaders = headers
  243. }
  244. }
  245. // Client is soap client
  246. type Client struct {
  247. opts *options
  248. headers []interface{}
  249. }
  250. // HTTPClient is a client which can make HTTP requests
  251. // An example implementation is net/http.Client
  252. type HTTPClient interface {
  253. Do(req *http.Request) (*http.Response, error)
  254. }
  255. // NewClient creates new SOAP client instance
  256. func NewClient(opt ...Option) *Client {
  257. opts := defaultOptions
  258. for _, o := range opt {
  259. o(&opts)
  260. }
  261. return &Client{
  262. opts: &opts,
  263. }
  264. }
  265. // AddHeader adds envelope header
  266. func (s *Client) AddHeader(header interface{}) {
  267. s.headers = append(s.headers, header)
  268. }
  269. // CallContext performs HTTP POST request with a context
  270. func (s *Client) CallContext(ctx context.Context, xaddr string, soapAction string, request, response interface{}) error {
  271. return s.call(ctx, xaddr, soapAction, request, response)
  272. }
  273. // Call performs HTTP POST request
  274. func (s *Client) Call(xaddr string, soapAction string, request, response interface{}) error {
  275. return s.call(context.Background(), xaddr, soapAction, request, response)
  276. }
  277. func (s *Client) call(ctx context.Context, xaddr string, soapAction string, request, response interface{}) error {
  278. envelope := SOAPEnvelope{}
  279. if s.headers != nil && len(s.headers) > 0 {
  280. envelope.Header.Headers = s.headers
  281. }
  282. envelope.Body.Content = request
  283. buffer := new(bytes.Buffer)
  284. var encoder SOAPEncoder
  285. encoder = xml.NewEncoder(buffer)
  286. if err := encoder.Encode(envelope); err != nil {
  287. return err
  288. }
  289. if err := encoder.Flush(); err != nil {
  290. return err
  291. }
  292. req, err := http.NewRequest("POST", xaddr, buffer)
  293. if err != nil {
  294. return err
  295. }
  296. if s.opts.auth != nil {
  297. req.SetBasicAuth(s.opts.auth.Login, s.opts.auth.Password)
  298. }
  299. req.WithContext(ctx)
  300. req.Header.Add("Content-Type", "application/soap+xml; charset=utf-8; action=\""+soapAction+"\"")
  301. req.Header.Add("Soapaction", "\""+soapAction+"\"")
  302. req.Header.Set("User-Agent", "videonext-onvif-go/0.1")
  303. if s.opts.httpHeaders != nil {
  304. for k, v := range s.opts.httpHeaders {
  305. req.Header.Set(k, v)
  306. }
  307. }
  308. req.Close = true
  309. client := s.opts.client
  310. if client == nil {
  311. tr := &http.Transport{
  312. TLSClientConfig: s.opts.tlsCfg,
  313. DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
  314. d := net.Dialer{Timeout: s.opts.timeout}
  315. return d.DialContext(ctx, network, addr)
  316. },
  317. TLSHandshakeTimeout: s.opts.tlshshaketimeout,
  318. }
  319. client = &http.Client{Timeout: s.opts.contimeout, Transport: tr}
  320. }
  321. res, err := client.Do(req)
  322. if err != nil {
  323. return err
  324. }
  325. defer res.Body.Close()
  326. respEnvelope := new(SOAPEnvelope)
  327. respEnvelope.Body = SOAPBody{Content: response}
  328. dec := NewDecoder(res.Body)
  329. if err := dec.Decode(respEnvelope); err != nil {
  330. return err
  331. }
  332. fault := respEnvelope.Body.Fault
  333. if fault != nil {
  334. return fault
  335. }
  336. return nil
  337. }