diff --git a/soap/soap.go b/soap/soap.go index 5817044..29a9b48 100644 --- a/soap/soap.go +++ b/soap/soap.go @@ -4,28 +4,153 @@ import ( "bytes" "crypto/tls" "encoding/xml" + "errors" "io/ioutil" "net" "net/http" "time" ) -type SOAPEnvelope struct { +// supported SOAP versions +const ( + SOAP11 = 11 + SOAP12 = 12 +) + +// SOAPVersion determines which version of SOAP protocol to use for requests +type SOAPVersion int + +// SOAPEnvelope is general representation of 3 types supported XML SOAP Envelopes +// for SOAP 1.1, 1.2 and custom user version +type SOAPEnvelope interface { + SetHeaders([]interface{}) + SetContent(content interface{}) + Fault() *SOAPFault +} + +// newSOAPEnvelope produces new SOAPEnvelope struct according passed params +func newSOAPEnvelope(c *Client, request bool) (SOAPEnvelope, error) { + // if user provided callbacks for custom constructors, use them + if request && c.opts.customReq != nil { + return c.opts.customReq(), nil + } + if !request && c.opts.customResp != nil { + return c.opts.customResp(), nil + } + + // overwise use default for standard headers + switch c.opts.version { + case SOAP11: + return new(SOAPEnvelope11), nil + case SOAP12: + return new(SOAPEnvelope12), nil + } + + return nil, errors.New("version is not supported") +} + +type SOAPEnvelope11 struct { XMLName xml.Name `xml:"http://schemas.xmlsoap.org/soap/envelope/ Envelope"` Headers []interface{} `xml:"http://schemas.xmlsoap.org/soap/envelope/ Header"` - Body SOAPBody + Body SOAPBody11 } -type SOAPBody struct { +func (s *SOAPEnvelope11) SetHeaders(headers []interface{}) { + s.Headers = headers +} + +func (s *SOAPEnvelope11) SetContent(content interface{}) { + s.Body = SOAPBody11{Content: content} +} + +func (s SOAPEnvelope11) Fault() *SOAPFault { + return s.Body.Fault +} + +type SOAPEnvelope12 struct { + XMLName xml.Name `xml:"http://www.w3.org/2003/05/soap-envelope Envelope"` + Headers []interface{} `xml:"http://www.w3.org/2003/05/soap-envelope Header"` + Body SOAPBody12 +} + +func (s *SOAPEnvelope12) SetHeaders(headers []interface{}) { + s.Headers = headers +} + +func (s *SOAPEnvelope12) SetContent(content interface{}) { + s.Body = SOAPBody12{Content: content} +} + +func (s SOAPEnvelope12) Fault() *SOAPFault { + return s.Body.Fault +} + +type SOAPBody11 struct { XMLName xml.Name `xml:"http://schemas.xmlsoap.org/soap/envelope/ Body"` Fault *SOAPFault `xml:",omitempty"` Content interface{} `xml:",omitempty"` } -// UnmarshalXML unmarshals SOAPBody xml -func (b *SOAPBody) UnmarshalXML(d *xml.Decoder, _ xml.StartElement) error { - if b.Content == nil { +func (s SOAPBody11) content() interface{} { + return s.Content +} + +func (s *SOAPBody11) setContent(c interface{}) { + s.Content = c +} + +func (s SOAPBody11) fault() *SOAPFault { + return s.Fault +} + +func (s *SOAPBody11) setFault(f *SOAPFault) { + s.Fault = f +} + +type SOAPBody interface { + content() interface{} + setContent(interface{}) + fault() *SOAPFault + setFault(*SOAPFault) +} + +type SOAPBody12 struct { + XMLName xml.Name `xml:"http://www.w3.org/2003/05/soap-envelope Body"` + + Fault *SOAPFault `xml:",omitempty"` + Content interface{} `xml:",omitempty"` +} + +func (s SOAPBody12) content() interface{} { + return s.Content +} + +func (s *SOAPBody12) setContent(c interface{}) { + s.Content = c +} + +func (s SOAPBody12) fault() *SOAPFault { + return s.Fault +} + +func (s *SOAPBody12) setFault(f *SOAPFault) { + s.Fault = f +} + +// UnmarshalXML unmarshals SOAPBody11 xml +func (b *SOAPBody11) UnmarshalXML(d *xml.Decoder, _ xml.StartElement) error { + return unmarshalSOAPBody(d, b) +} + +// UnmarshalXML unmarshals SOAPBody12 xml +func (b *SOAPBody12) UnmarshalXML(d *xml.Decoder, _ xml.StartElement) error { + return unmarshalSOAPBody(d, b) +} + +// unmarshalSOAPBody is a common code for both SOAP11 and SOAP12 unmarshal funcs +func unmarshalSOAPBody(d *xml.Decoder, b SOAPBody) error { + if b.content() == nil { return xml.UnmarshalError("Content must be a pointer to a struct") } @@ -50,17 +175,17 @@ Loop: if consumed { return xml.UnmarshalError("Found multiple elements inside SOAP body; not wrapped-document/literal WS-I compliant") } else if se.Name.Space == "http://schemas.xmlsoap.org/soap/envelope/" && se.Name.Local == "Fault" { - b.Fault = &SOAPFault{} - b.Content = nil + b.setFault(&SOAPFault{}) + b.setContent(nil) - err = d.DecodeElement(b.Fault, &se) + err = d.DecodeElement(b.fault(), &se) if err != nil { return err } consumed = true } else { - if err = d.DecodeElement(b.Content, &se); err != nil { + if err = d.DecodeElement(b.content(), &se); err != nil { return err } @@ -151,12 +276,16 @@ type options struct { tlshshaketimeout time.Duration client HTTPClient httpHeaders map[string]string + version SOAPVersion + customReq func() SOAPEnvelope + customResp func() SOAPEnvelope } var defaultOptions = options{ timeout: time.Duration(30 * time.Second), contimeout: time.Duration(90 * time.Second), tlshshaketimeout: time.Duration(15 * time.Second), + version: SOAP11, } // A Option sets options such as credentials, tls, etc. @@ -216,6 +345,21 @@ func WithHTTPHeaders(headers map[string]string) Option { } } +// WithSOAPVersion is an Option to set SOAP protocol version to use +func WithSOAPVersion(version SOAPVersion) Option { + return func(o *options) { + o.version = version + } +} + +// WithCustomRequester is an Option to set specific SOAP Envelope contructor for +// broken services (used only for send requests) +func WithCustomRequester(req func() SOAPEnvelope) Option { + return func(o *options) { + o.customReq = req + } +} + // Client is soap client type Client struct { url string @@ -248,15 +392,17 @@ func (s *Client) AddHeader(header interface{}) { // Call performs HTTP POST request func (s *Client) Call(soapAction string, request, response interface{}) error { - envelope := SOAPEnvelope{} + envelope, err := newSOAPEnvelope(s, true /* request */) + if err != nil { + return nil + } if s.headers != nil && len(s.headers) > 0 { - envelope.Headers = s.headers + envelope.SetHeaders(s.headers) } - envelope.Body.Content = request + envelope.SetContent(request) buffer := new(bytes.Buffer) - encoder := xml.NewEncoder(buffer) if err := encoder.Encode(envelope); err != nil { @@ -275,7 +421,12 @@ func (s *Client) Call(soapAction string, request, response interface{}) error { req.SetBasicAuth(s.opts.auth.Login, s.opts.auth.Password) } - req.Header.Add("Content-Type", "text/xml; charset=\"utf-8\"") + switch s.opts.version { + case SOAP11: + req.Header.Add("Content-Type", "text/xml; charset=\"utf-8\"") + case SOAP12: + req.Header.Add("Content-Type", "application/soap+xml") + } req.Header.Add("SOAPAction", soapAction) req.Header.Set("User-Agent", "gowsdl/0.1") if s.opts.httpHeaders != nil { @@ -311,14 +462,17 @@ func (s *Client) Call(soapAction string, request, response interface{}) error { return nil } - respEnvelope := new(SOAPEnvelope) - respEnvelope.Body = SOAPBody{Content: response} + respEnvelope, err := newSOAPEnvelope(s, false /* request */) + if err != nil { + return nil + } + respEnvelope.SetContent(response) err = xml.Unmarshal(rawbody, respEnvelope) if err != nil { return err } - fault := respEnvelope.Body.Fault + fault := respEnvelope.Fault() if fault != nil { return fault }