| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418 |
- package soap
- import (
- "bytes"
- "context"
- "crypto/sha1"
- "crypto/tls"
- "encoding/base64"
- "encoding/xml"
- "math/rand"
- "net"
- "net/http"
- "strings"
- "time"
- )
- type SOAPEncoder interface {
- Encode(v interface{}) error
- Flush() error
- }
- type SOAPDecoder interface {
- Decode(v interface{}) error
- }
- type SOAPHeader struct {
- XMLName xml.Name `xml:"http://www.w3.org/2003/05/soap-envelope Header"`
- Headers []interface{} `xml:"http://www.w3.org/2003/05/soap-envelope Header"`
- }
- type SOAPEnvelope struct {
- XMLName xml.Name `xml:"http://www.w3.org/2003/05/soap-envelope Envelope"`
- Header SOAPHeader
- Body SOAPBody
- }
- type SOAPBody struct {
- XMLName xml.Name `xml:"http://www.w3.org/2003/05/soap-envelope Body"`
- Fault *SOAPFault `xml:",omitempty"`
- Content interface{} `xml:",omitempty"`
- }
- // UnmarshalXML unmarshals SOAPBody xml
- func (b *SOAPBody) UnmarshalXML(d *Decoder, _ StartElement) error {
- if b.Content == nil {
- return xml.UnmarshalError("Content must be a pointer to a struct")
- }
- var (
- token xml.Token
- err error
- consumed bool
- )
- Loop:
- for {
- if token, err = d.Token(); err != nil {
- return err
- }
- if token == nil {
- break
- }
- switch se := token.(type) {
- case StartElement:
- if consumed {
- return xml.UnmarshalError("Found multiple elements inside SOAP body; not wrapped-document/literal WS-I compliant")
- } else if se.Name.Space == "http://www.w3.org/2003/05/soap-envelope" && se.Name.Local == "Fault" {
- b.Fault = &SOAPFault{}
- b.Content = nil
- err = d.DecodeElement(b.Fault, &se)
- if err != nil {
- return err
- }
- consumed = true
- } else {
- if err = d.DecodeElement(b.Content, &se); err != nil {
- return err
- }
- consumed = true
- }
- case EndElement:
- break Loop
- }
- }
- return nil
- }
- type SOAPFaultSubCode struct {
- XMLName xml.Name `xml:"http://www.w3.org/2003/05/soap-envelope Subcode"`
- Value string
- }
- type SOAPFaultCode struct {
- XMLName xml.Name `xml:"http://www.w3.org/2003/05/soap-envelope Code"`
- Value string
- Subcode SOAPFaultSubCode
- }
- type SOAPFaultReason struct {
- XMLName xml.Name `xml:"http://www.w3.org/2003/05/soap-envelope Reason"`
- Text string
- }
- type SOAPFaultDetail struct {
- XMLName xml.Name `xml:"http://www.w3.org/2003/05/soap-envelope Detail"`
- Text string
- }
- type SOAPFault struct {
- XMLName xml.Name `xml:"http://www.w3.org/2003/05/soap-envelope Fault"`
- Code SOAPFaultCode
- Reason SOAPFaultReason `xml:",omitempty"`
- Detail SOAPFaultDetail `xml:",omitempty"`
- }
- func (f *SOAPFault) Error() string {
- s := f.Reason.Text
- if f.Detail.Text != "" {
- s += ". Details: " + f.Detail.Text
- }
- if s == "" {
- if f.Code.Value != "" {
- s = f.Code.Value + ". "
- }
- if f.Code.Subcode.Value != "" {
- s += f.Code.Subcode.Value
- }
- }
- return s
- }
- const (
- // Predefined WSS namespaces to be used in
- WssNsWSSE string = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"
- WssNsWSU string = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"
- WssNsType string = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText"
- mtomContentType string = `multipart/related; start-info="application/soap+xml"; type="application/xop+xml"; boundary="%s"`
- )
- type WSSPassword struct {
- Type string `xml:",attr"`
- Value string `xml:",chardata"`
- }
- type WSSNonce struct {
- EncodingType string `xml:",attr"`
- Value string `xml:",chardata"`
- }
- type WSSCreated struct {
- XMLName xml.Name `xml:"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd Created"`
- Value string `xml:",chardata"`
- }
- type WSSUsernameToken struct {
- Username string
- Password WSSPassword
- Nonce WSSNonce
- Created WSSCreated
- }
- type WSSSecurityHeader struct {
- XMLName xml.Name `xml:"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd Security"`
- MustUnderstand string `xml:"mustUnderstand,attr"`
- UsernameToken WSSUsernameToken
- }
- const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
- func randomString(n int) string {
- sb := strings.Builder{}
- sb.Grow(n)
- for i := 0; i < n; i++ {
- sb.WriteByte(charset[rand.Intn(len(charset))])
- }
- return sb.String()
- }
- // NewWSSSecurityHeader creates WSSSecurityHeader instance
- func NewWSSSecurityHeader(user, pass string) *WSSSecurityHeader {
- hdr := &WSSSecurityHeader{MustUnderstand: "1"}
- // Username
- hdr.UsernameToken.Username = user
- // Created
- hdr.UsernameToken.Created.Value = time.Now().Format("2006-01-02T15:04:05.999") + "Z"
- // Nonce
- b := make([]byte, 16)
- rand.Read(b)
- //nonce := fmt.Sprintf("%x-%x-%x-%x-%x",
- // b[0:4], b[4:6], b[6:8], b[8:10], b[10:])
- rand.Seed(time.Now().UnixNano())
- nonce := randomString(20)
- hdr.UsernameToken.Nonce.EncodingType = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary"
- hdr.UsernameToken.Nonce.Value = base64.StdEncoding.EncodeToString([]byte(nonce))
- // Password
- h := sha1.New()
- h.Write([]byte(nonce + hdr.UsernameToken.Created.Value + pass))
- hdr.UsernameToken.Password.Type = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest"
- hdr.UsernameToken.Password.Value = base64.StdEncoding.EncodeToString(h.Sum(nil))
- return hdr
- }
- type basicAuth struct {
- Login string
- Password string
- }
- type options struct {
- tlsCfg *tls.Config
- auth *basicAuth
- timeout time.Duration
- contimeout time.Duration
- tlshshaketimeout time.Duration
- client HTTPClient
- httpHeaders map[string]string
- }
- var defaultOptions = options{
- timeout: time.Duration(30 * time.Second),
- contimeout: time.Duration(90 * time.Second),
- tlshshaketimeout: time.Duration(15 * time.Second),
- }
- // A Option sets options such as credentials, tls, etc.
- type Option func(*options)
- // WithHTTPClient is an Option to set the HTTP client to use
- // This cannot be used with WithTLSHandshakeTimeout, WithTLS,
- // WithTimeout options
- func WithHTTPClient(c HTTPClient) Option {
- return func(o *options) {
- o.client = c
- }
- }
- // WithTLSHandshakeTimeout is an Option to set default tls handshake timeout
- // This option cannot be used with WithHTTPClient
- func WithTLSHandshakeTimeout(t time.Duration) Option {
- return func(o *options) {
- o.tlshshaketimeout = t
- }
- }
- // WithRequestTimeout is an Option to set default end-end connection timeout
- // This option cannot be used with WithHTTPClient
- func WithRequestTimeout(t time.Duration) Option {
- return func(o *options) {
- o.contimeout = t
- }
- }
- // WithBasicAuth is an Option to set BasicAuth
- func WithBasicAuth(login, password string) Option {
- return func(o *options) {
- o.auth = &basicAuth{Login: login, Password: password}
- }
- }
- // WithTLS is an Option to set tls config
- // This option cannot be used with WithHTTPClient
- func WithTLS(tls *tls.Config) Option {
- return func(o *options) {
- o.tlsCfg = tls
- }
- }
- // WithTimeout is an Option to set default HTTP dial timeout
- func WithTimeout(t time.Duration) Option {
- return func(o *options) {
- o.timeout = t
- }
- }
- // WithHTTPHeaders is an Option to set global HTTP headers for all requests
- func WithHTTPHeaders(headers map[string]string) Option {
- return func(o *options) {
- o.httpHeaders = headers
- }
- }
- // Client is soap client
- type Client struct {
- opts *options
- headers []interface{}
- }
- // HTTPClient is a client which can make HTTP requests
- // An example implementation is net/http.Client
- type HTTPClient interface {
- Do(req *http.Request) (*http.Response, error)
- }
- // NewClient creates new SOAP client instance
- func NewClient(opt ...Option) *Client {
- opts := defaultOptions
- for _, o := range opt {
- o(&opts)
- }
- return &Client{
- opts: &opts,
- }
- }
- // AddHeader adds envelope header
- func (s *Client) AddHeader(header interface{}) {
- s.headers = append(s.headers, header)
- }
- // CallContext performs HTTP POST request with a context
- func (s *Client) CallContext(ctx context.Context, xaddr string, soapAction string, request, response interface{}) error {
- return s.call(ctx, xaddr, soapAction, request, response)
- }
- // Call performs HTTP POST request
- func (s *Client) Call(xaddr string, soapAction string, request, response interface{}) error {
- return s.call(context.Background(), xaddr, soapAction, request, response)
- }
- func (s *Client) call(ctx context.Context, xaddr string, soapAction string, request, response interface{}) error {
- envelope := SOAPEnvelope{}
- if s.headers != nil && len(s.headers) > 0 {
- envelope.Header.Headers = s.headers
- }
- envelope.Body.Content = request
- buffer := new(bytes.Buffer)
- var encoder SOAPEncoder
- encoder = xml.NewEncoder(buffer)
- if err := encoder.Encode(envelope); err != nil {
- return err
- }
- if err := encoder.Flush(); err != nil {
- return err
- }
- req, err := http.NewRequest("POST", xaddr, buffer)
- if err != nil {
- return err
- }
- if s.opts.auth != nil {
- req.SetBasicAuth(s.opts.auth.Login, s.opts.auth.Password)
- }
- req.WithContext(ctx)
- req.Header.Add("Content-Type", "application/soap+xml; charset=utf-8; action=\""+soapAction+"\"")
- req.Header.Add("Soapaction", "\""+soapAction+"\"")
- req.Header.Set("User-Agent", "videonext-onvif-go/0.1")
- if s.opts.httpHeaders != nil {
- for k, v := range s.opts.httpHeaders {
- req.Header.Set(k, v)
- }
- }
- req.Close = true
- client := s.opts.client
- if client == nil {
- tr := &http.Transport{
- TLSClientConfig: s.opts.tlsCfg,
- DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
- d := net.Dialer{Timeout: s.opts.timeout}
- return d.DialContext(ctx, network, addr)
- },
- TLSHandshakeTimeout: s.opts.tlshshaketimeout,
- }
- client = &http.Client{Timeout: s.opts.contimeout, Transport: tr}
- }
- res, err := client.Do(req)
- if err != nil {
- return err
- }
- defer res.Body.Close()
- respEnvelope := new(SOAPEnvelope)
- respEnvelope.Body = SOAPBody{Content: response}
- dec := NewDecoder(res.Body)
- if err := dec.Decode(respEnvelope); err != nil {
- return err
- }
- fault := respEnvelope.Body.Fault
- if fault != nil {
- return fault
- }
- return nil
- }
|