soap.go 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418
  1. package soap
  2. import (
  3. "bytes"
  4. "context"
  5. crand "crypto/rand"
  6. "crypto/sha1"
  7. "crypto/tls"
  8. "encoding/base64"
  9. "encoding/xml"
  10. "math/rand"
  11. "net"
  12. "net/http"
  13. "strings"
  14. "time"
  15. )
  16. type SOAPEncoder interface {
  17. Encode(v interface{}) error
  18. Flush() error
  19. }
  20. type SOAPDecoder interface {
  21. Decode(v interface{}) error
  22. }
  23. type SOAPHeader struct {
  24. XMLName xml.Name `xml:"http://www.w3.org/2003/05/soap-envelope Header"`
  25. Headers []interface{} `xml:"http://www.w3.org/2003/05/soap-envelope Header"`
  26. }
  27. type SOAPEnvelope struct {
  28. XMLName xml.Name `xml:"http://www.w3.org/2003/05/soap-envelope Envelope"`
  29. Header SOAPHeader
  30. Body SOAPBody
  31. }
  32. type SOAPBody struct {
  33. XMLName xml.Name `xml:"http://www.w3.org/2003/05/soap-envelope Body"`
  34. Fault *SOAPFault `xml:",omitempty"`
  35. Content interface{} `xml:",omitempty"`
  36. }
  37. // UnmarshalXML unmarshals SOAPBody xml
  38. func (b *SOAPBody) UnmarshalXML(d *Decoder, _ StartElement) error {
  39. if b.Content == nil {
  40. return xml.UnmarshalError("Content must be a pointer to a struct")
  41. }
  42. var (
  43. token xml.Token
  44. err error
  45. consumed bool
  46. )
  47. Loop:
  48. for {
  49. if token, err = d.Token(); err != nil {
  50. return err
  51. }
  52. if token == nil {
  53. break
  54. }
  55. switch se := token.(type) {
  56. case StartElement:
  57. if consumed {
  58. return xml.UnmarshalError("Found multiple elements inside SOAP body; not wrapped-document/literal WS-I compliant")
  59. } else if se.Name.Space == "http://www.w3.org/2003/05/soap-envelope" && se.Name.Local == "Fault" {
  60. b.Fault = &SOAPFault{}
  61. b.Content = nil
  62. err = d.DecodeElement(b.Fault, &se)
  63. if err != nil {
  64. return err
  65. }
  66. consumed = true
  67. } else {
  68. if err = d.DecodeElement(b.Content, &se); err != nil {
  69. return err
  70. }
  71. consumed = true
  72. }
  73. case EndElement:
  74. break Loop
  75. }
  76. }
  77. return nil
  78. }
  79. type SOAPFaultSubCode struct {
  80. XMLName xml.Name `xml:"http://www.w3.org/2003/05/soap-envelope Subcode"`
  81. Value string
  82. }
  83. type SOAPFaultCode struct {
  84. XMLName xml.Name `xml:"http://www.w3.org/2003/05/soap-envelope Code"`
  85. Value string
  86. Subcode SOAPFaultSubCode
  87. }
  88. type SOAPFaultReason struct {
  89. XMLName xml.Name `xml:"http://www.w3.org/2003/05/soap-envelope Reason"`
  90. Text string
  91. }
  92. type SOAPFaultDetail struct {
  93. XMLName xml.Name `xml:"http://www.w3.org/2003/05/soap-envelope Detail"`
  94. Text string
  95. }
  96. type SOAPFault struct {
  97. XMLName xml.Name `xml:"http://www.w3.org/2003/05/soap-envelope Fault"`
  98. Code SOAPFaultCode
  99. Reason SOAPFaultReason `xml:",omitempty"`
  100. Detail SOAPFaultDetail `xml:",omitempty"`
  101. }
  102. func (f *SOAPFault) Error() string {
  103. s := f.Reason.Text
  104. if f.Detail.Text != "" {
  105. s += ". Details: " + f.Detail.Text
  106. }
  107. if s == "" {
  108. if f.Code.Value != "" {
  109. s = f.Code.Value + ". "
  110. }
  111. if f.Code.Subcode.Value != "" {
  112. s += f.Code.Subcode.Value
  113. }
  114. }
  115. return s
  116. }
  117. const (
  118. // Predefined WSS namespaces to be used in
  119. WssNsWSSE string = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"
  120. WssNsWSU string = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"
  121. WssNsType string = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText"
  122. mtomContentType string = `multipart/related; start-info="application/soap+xml"; type="application/xop+xml"; boundary="%s"`
  123. )
  124. type WSSPassword struct {
  125. Type string `xml:",attr"`
  126. Value string `xml:",chardata"`
  127. }
  128. type WSSNonce struct {
  129. EncodingType string `xml:",attr"`
  130. Value string `xml:",chardata"`
  131. }
  132. type WSSCreated struct {
  133. XMLName xml.Name `xml:"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd Created"`
  134. Value string `xml:",chardata"`
  135. }
  136. type WSSUsernameToken struct {
  137. Username string
  138. Password WSSPassword
  139. Nonce WSSNonce
  140. Created WSSCreated
  141. }
  142. type WSSSecurityHeader struct {
  143. XMLName xml.Name `xml:"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd Security"`
  144. MustUnderstand string `xml:"mustUnderstand,attr"`
  145. UsernameToken WSSUsernameToken
  146. }
  147. const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
  148. func randomString(n int) string {
  149. sb := strings.Builder{}
  150. sb.Grow(n)
  151. for i := 0; i < n; i++ {
  152. sb.WriteByte(charset[rand.Intn(len(charset))])
  153. }
  154. return sb.String()
  155. }
  156. // NewWSSSecurityHeader creates WSSSecurityHeader instance
  157. func NewWSSSecurityHeader(user, pass string) *WSSSecurityHeader {
  158. hdr := &WSSSecurityHeader{MustUnderstand: "1"}
  159. // Username
  160. hdr.UsernameToken.Username = user
  161. // Created
  162. hdr.UsernameToken.Created.Value = time.Now().Format("2006-01-02T15:04:05.999") + "Z"
  163. // Nonce
  164. b := make([]byte, 16)
  165. crand.Read(b)
  166. //nonce := fmt.Sprintf("%x-%x-%x-%x-%x",
  167. // b[0:4], b[4:6], b[6:8], b[8:10], b[10:])
  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. }