├── .gitignore ├── LICENSE ├── README.md ├── client.go ├── examples ├── client.go └── simple-server.go ├── server.go └── soap.go /.gitignore: -------------------------------------------------------------------------------- 1 | .* 2 | !.git* 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Foomo web framework 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SOAP is dead - long live SOAP 2 | 3 | First of all do not write SOAP services if you can avoid it! It is over. 4 | 5 | If you can not avoid it this package might help. 6 | 7 | ## Service 8 | 9 | ```go 10 | package main 11 | 12 | import ( 13 | "encoding/xml" 14 | "fmt" 15 | "net/http" 16 | 17 | "github.com/foomo/soap" 18 | ) 19 | 20 | // FooRequest a simple request 21 | type FooRequest struct { 22 | XMLName xml.Name `xml:"fooRequest"` 23 | Foo string 24 | } 25 | 26 | // FooResponse a simple response 27 | type FooResponse struct { 28 | Bar string 29 | } 30 | 31 | // RunServer run a little demo server 32 | func RunServer() { 33 | soapServer := soap.NewServer() 34 | soapServer.HandleOperation( 35 | // SOAPAction 36 | "operationFoo", 37 | // tagname of soap body content 38 | "fooRequest", 39 | // RequestFactoryFunc - give the server sth. to unmarshal the request into 40 | func() interface{} { 41 | return &FooRequest{} 42 | }, 43 | // OperationHandlerFunc - do something 44 | func(request interface{}, w http.ResponseWriter, httpRequest *http.Request) (response interface{}, err error) { 45 | fooRequest := request.(*FooRequest) 46 | fooResponse := &FooResponse{ 47 | Bar: "Hello " + fooRequest.Foo, 48 | } 49 | response = fooResponse 50 | return 51 | }, 52 | ) 53 | err := soapServer.ListenAndServe(":8080") 54 | fmt.Println("exiting with error", err) 55 | } 56 | 57 | func main() { 58 | RunServer() 59 | } 60 | ``` 61 | 62 | ## Client 63 | 64 | ```go 65 | package main 66 | 67 | import ( 68 | "encoding/xml" 69 | "log" 70 | 71 | "github.com/foomo/soap" 72 | ) 73 | 74 | // FooRequest a simple request 75 | type FooRequest struct { 76 | XMLName xml.Name `xml:"fooRequest"` 77 | Foo string 78 | } 79 | 80 | // FooResponse a simple response 81 | type FooResponse struct { 82 | Bar string 83 | } 84 | 85 | func main() { 86 | soap.Verbose = true 87 | client := soap.NewClient("http://127.0.0.1:8080/", nil, nil) 88 | response := &FooResponse{} 89 | httpResponse, err := client.Call("operationFoo", &FooRequest{Foo: "hello i am foo"}, response) 90 | if err != nil { 91 | panic(err) 92 | } 93 | log.Println(response.Bar, httpResponse.Status) 94 | } 95 | 96 | ``` 97 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | package soap 2 | 3 | import ( 4 | "bytes" 5 | "encoding/xml" 6 | "errors" 7 | "io" 8 | "io/ioutil" 9 | "log" 10 | "mime" 11 | "mime/multipart" 12 | "net" 13 | "net/http" 14 | "reflect" 15 | "strings" 16 | "time" 17 | ) 18 | 19 | // ClientDialTimeout default timeout 30s 20 | var ClientDialTimeout = time.Duration(30 * time.Second) 21 | 22 | // UserAgent is the default user agent 23 | var UserAgent = "go-soap-0.1" 24 | 25 | // XMLMarshaller lets you inject your favourite custom xml implementation 26 | type XMLMarshaller interface { 27 | Marshal(v interface{}) ([]byte, error) 28 | Unmarshal(xml []byte, v interface{}) error 29 | } 30 | 31 | type defaultMarshaller struct { 32 | } 33 | 34 | func (dm *defaultMarshaller) Marshal(v interface{}) (xmlBytes []byte, err error) { 35 | return xml.MarshalIndent(v, "", " ") 36 | } 37 | 38 | func (dm *defaultMarshaller) Unmarshal(xmlBytes []byte, v interface{}) error { 39 | return xml.Unmarshal(xmlBytes, v) 40 | } 41 | 42 | func newDefaultMarshaller() XMLMarshaller { 43 | return &defaultMarshaller{} 44 | } 45 | 46 | func dialTimeout(network, addr string) (net.Conn, error) { 47 | return net.DialTimeout(network, addr, ClientDialTimeout) 48 | } 49 | 50 | // BasicAuth credentials for the client 51 | type BasicAuth struct { 52 | Login string 53 | Password string 54 | } 55 | 56 | // Client generic SOAP client 57 | type Client struct { 58 | url string 59 | tls bool 60 | auth *BasicAuth 61 | tr *http.Transport 62 | Marshaller XMLMarshaller 63 | ContentType string 64 | SoapVersion string 65 | } 66 | 67 | // NewClient constructor. SOAP 1.1 is used by default. Switch to SOAP 1.2 with UseSoap12() 68 | func NewClient(url string, auth *BasicAuth, tr *http.Transport) *Client { 69 | return &Client{ 70 | url: url, 71 | auth: auth, 72 | tr: tr, 73 | Marshaller: newDefaultMarshaller(), 74 | ContentType: SoapContentType11, // default is SOAP 1.1 75 | SoapVersion: SoapVersion11, 76 | } 77 | } 78 | 79 | func (c *Client) UseSoap11() { 80 | c.SoapVersion = SoapVersion11 81 | c.ContentType = SoapContentType11 82 | } 83 | 84 | func (c *Client) UseSoap12() { 85 | c.SoapVersion = SoapVersion12 86 | c.ContentType = SoapContentType12 87 | } 88 | 89 | // Call make a SOAP call 90 | func (c *Client) Call(soapAction string, request, response interface{}) (httpResponse *http.Response, err error) { 91 | 92 | envelope := Envelope{} 93 | 94 | envelope.Body.Content = request 95 | 96 | xmlBytes, err := c.Marshaller.Marshal(envelope) 97 | if err != nil { 98 | return nil, err 99 | } 100 | // Adjust namespaces for SOAP 1.2 101 | if c.SoapVersion == SoapVersion12 { 102 | tmp := string(xmlBytes) 103 | tmp = strings.Replace(tmp, NamespaceSoap11, NamespaceSoap12, -1) 104 | xmlBytes = []byte(tmp) 105 | } 106 | //log.Println(string(xmlBytes)) 107 | 108 | //l("SOAP Client Call() => Marshalled Request\n", string(xmlBytes)) 109 | 110 | req, err := http.NewRequest("POST", c.url, bytes.NewBuffer(xmlBytes)) 111 | if err != nil { 112 | return nil, err 113 | } 114 | if c.auth != nil { 115 | req.SetBasicAuth(c.auth.Login, c.auth.Password) 116 | } 117 | 118 | req.Header.Add("Content-Type", c.ContentType) 119 | req.Header.Set("User-Agent", UserAgent) 120 | 121 | if soapAction != "" { 122 | req.Header.Add("SOAPAction", soapAction) 123 | } 124 | 125 | req.Close = true 126 | tr := c.tr 127 | if tr == nil { 128 | tr = http.DefaultTransport.(*http.Transport) 129 | } 130 | client := &http.Client{Transport: tr} 131 | l("POST to", c.url, "with\n", string(xmlBytes)) 132 | l("Header") 133 | LogJSON(req.Header) 134 | httpResponse, err = client.Do(req) 135 | if err != nil { 136 | return nil, err 137 | } 138 | 139 | defer httpResponse.Body.Close() 140 | 141 | l("\n\n## Response header:\n", httpResponse.Header) 142 | 143 | mediaType, params, err := mime.ParseMediaType(httpResponse.Header.Get("Content-Type")) 144 | if err != nil { 145 | l("WARNING:", err) 146 | } 147 | l("MIMETYPE:", mediaType) 148 | var rawbody = []byte{} 149 | if strings.HasPrefix(mediaType, "multipart/") { // MULTIPART MESSAGE 150 | mr := multipart.NewReader(httpResponse.Body, params["boundary"]) 151 | // If this is a multipart message, search for the soapy part 152 | foundSoap := false 153 | for { 154 | p, err := mr.NextPart() 155 | if err == io.EOF { 156 | return nil, err 157 | } 158 | if err != nil { 159 | return nil, err 160 | } 161 | slurp, err := ioutil.ReadAll(p) 162 | if err != nil { 163 | return nil, err 164 | } 165 | if strings.HasPrefix(string(slurp), " 0 { 241 | n = level - startLevel - 1 242 | } 243 | out.Write([]byte(strings.Repeat(indent, n))) 244 | } 245 | lf := func() { 246 | out.Write([]byte("\n")) 247 | } 248 | 249 | lastWasStart := false 250 | lastWasCharData := false 251 | lastWasEnd := false 252 | 253 | for token, err := d.Token(); token != nil && err == nil; token, err = d.Token() { 254 | r := reflect.ValueOf(token) 255 | switch r.Type() { 256 | case typeStart: 257 | lastWasCharData = false 258 | se := token.(xml.StartElement) 259 | if lastWasEnd || lastWasStart { 260 | lf() 261 | } 262 | lastWasStart = true 263 | ind() 264 | elementName := se.Name.Local 265 | 266 | if level > startLevel { 267 | out.WriteString("<" + elementName) 268 | out.WriteString(">") 269 | } 270 | 271 | level++ 272 | lastWasEnd = false 273 | case typeCharData: 274 | lastWasCharData = true 275 | _ = lastWasCharData 276 | lastWasStart = false 277 | cdata := token.(xml.CharData) 278 | xml.EscapeText(out, cdata) 279 | lastWasEnd = false 280 | case typeEnd: 281 | level-- 282 | if lastWasEnd { 283 | lf() 284 | ind() 285 | } 286 | lastWasEnd = true 287 | lastWasStart = false 288 | end := token.(xml.EndElement) 289 | if level > startLevel { 290 | endTagName := end.Name.Local 291 | out.WriteString("") 292 | } 293 | 294 | } 295 | } 296 | return strings.Trim(string(out.Bytes()), " \n") 297 | } 298 | -------------------------------------------------------------------------------- /examples/client.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/xml" 5 | "log" 6 | 7 | "github.com/foomo/soap" 8 | ) 9 | 10 | // FooRequest a simple request 11 | type FooRequest struct { 12 | XMLName xml.Name `xml:"fooRequest"` 13 | Foo string 14 | } 15 | 16 | // FooResponse a simple response 17 | type FooResponse struct { 18 | Bar string 19 | } 20 | 21 | func main() { 22 | soap.Verbose = true 23 | client := soap.NewClient("http://127.0.0.1:8080/", nil, nil) 24 | response := &FooResponse{} 25 | httpResponse, err := client.Call("operationFoo", &FooRequest{Foo: "hello i am foo"}, response) 26 | if err != nil { 27 | panic(err) 28 | } 29 | log.Println(response.Bar, httpResponse.Status) 30 | } 31 | -------------------------------------------------------------------------------- /examples/simple-server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/xml" 5 | "fmt" 6 | "net/http" 7 | 8 | "github.com/foomo/soap" 9 | ) 10 | 11 | // FooRequest a simple request 12 | type FooRequest struct { 13 | XMLName xml.Name `xml:"fooRequest"` 14 | Foo string 15 | } 16 | 17 | // FooResponse a simple response 18 | type FooResponse struct { 19 | Bar string 20 | } 21 | 22 | // RunServer run a little demo server 23 | func RunServer() { 24 | soapServer := soap.NewServer() 25 | soapServer.HandleOperation( 26 | // SOAPAction 27 | "operationFoo", 28 | // tagname of soap body content 29 | "fooRequest", 30 | // RequestFactoryFunc - give the server sth. to unmarshal the request into 31 | func() interface{} { 32 | return &FooRequest{} 33 | }, 34 | // OperationHandlerFunc - do something 35 | func(request interface{}, w http.ResponseWriter, httpRequest *http.Request) (response interface{}, err error) { 36 | fooRequest := request.(*FooRequest) 37 | fooResponse := &FooResponse{ 38 | Bar: "Hello \"" + fooRequest.Foo + "\"", 39 | } 40 | response = fooResponse 41 | return 42 | }, 43 | ) 44 | err := soapServer.ListenAndServe(":8080") 45 | fmt.Println("exiting with error", err) 46 | } 47 | 48 | func main() { 49 | // see what is going on 50 | soap.Verbose = true 51 | RunServer() 52 | } 53 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | package soap 2 | 3 | import ( 4 | "encoding/json" 5 | "encoding/xml" 6 | "errors" 7 | "fmt" 8 | "io/ioutil" 9 | "net/http" 10 | "strings" 11 | ) 12 | 13 | // OperationHandlerFunc runs the actual business logic - request is whatever you constructed in RequestFactoryFunc 14 | type OperationHandlerFunc func(request interface{}, w http.ResponseWriter, httpRequest *http.Request) (response interface{}, err error) 15 | 16 | // RequestFactoryFunc constructs a request object for OperationHandlerFunc 17 | type RequestFactoryFunc func() interface{} 18 | 19 | type dummyContent struct{} 20 | 21 | type operationHander struct { 22 | requestFactory RequestFactoryFunc 23 | handler OperationHandlerFunc 24 | } 25 | 26 | type responseWriter struct { 27 | w http.ResponseWriter 28 | outputStarted bool 29 | } 30 | 31 | func (w *responseWriter) Header() http.Header { 32 | return w.w.Header() 33 | } 34 | func (w *responseWriter) Write(b []byte) (int, error) { 35 | w.outputStarted = true 36 | if Verbose { 37 | l("writing response: " + string(b)) 38 | } 39 | return w.w.Write(b) 40 | } 41 | 42 | func (w *responseWriter) WriteHeader(code int) { 43 | w.w.WriteHeader(code) 44 | } 45 | 46 | // Server a SOAP server, which can be run standalone or used as a http.HandlerFunc 47 | type Server struct { 48 | handlers map[string]map[string]map[string]*operationHander 49 | Marshaller XMLMarshaller 50 | ContentType string 51 | SoapVersion string 52 | } 53 | 54 | // NewServer construct a new SOAP server 55 | func NewServer() *Server { 56 | s := &Server{ 57 | handlers: make(map[string]map[string]map[string]*operationHander), 58 | Marshaller: newDefaultMarshaller(), 59 | ContentType: SoapContentType11, 60 | SoapVersion: SoapVersion11, 61 | } 62 | return s 63 | } 64 | 65 | func (s *Server) UseSoap11() { 66 | s.SoapVersion = SoapVersion11 67 | s.ContentType = SoapContentType11 68 | } 69 | 70 | func (s *Server) UseSoap12() { 71 | s.SoapVersion = SoapVersion12 72 | s.ContentType = SoapContentType12 73 | } 74 | 75 | // RegisterHandler register to handle an operation 76 | func (s *Server) RegisterHandler(path string, action string, messageType string, requestFactory RequestFactoryFunc, operationHandlerFunc OperationHandlerFunc) { 77 | _, pathHandlersOK := s.handlers[path] 78 | if !pathHandlersOK { 79 | s.handlers[path] = make(map[string]map[string]*operationHander) 80 | } 81 | _, ok := s.handlers[path][action] 82 | if !ok { 83 | s.handlers[path][action] = make(map[string]*operationHander) 84 | } 85 | s.handlers[path][action][messageType] = &operationHander{ 86 | handler: operationHandlerFunc, 87 | requestFactory: requestFactory, 88 | } 89 | } 90 | 91 | func (s *Server) handleError(err error, w http.ResponseWriter) { 92 | // has to write a soap fault 93 | l("handling error:", err) 94 | responseEnvelope := &Envelope{ 95 | Body: Body{ 96 | Content: &Fault{ 97 | String: err.Error(), 98 | }, 99 | }, 100 | } 101 | xmlBytes, xmlErr := s.Marshaller.Marshal(responseEnvelope) 102 | if xmlErr != nil { 103 | w.WriteHeader(http.StatusInternalServerError) 104 | w.Write([]byte("could not marshal soap fault for: " + err.Error() + " xmlError: " + xmlErr.Error())) 105 | } else { 106 | addSOAPHeader(w, len(xmlBytes), s.ContentType) 107 | w.Write(xmlBytes) 108 | } 109 | } 110 | 111 | // WriteHeader first sets header like content-type and then writes the header 112 | func (s *Server) WriteHeader(w http.ResponseWriter, code int) { 113 | setContentType(w, s.ContentType) 114 | w.WriteHeader(code) 115 | } 116 | 117 | func setContentType(w http.ResponseWriter, contentType string) { 118 | w.Header().Set("Content-Type", contentType) 119 | } 120 | 121 | func addSOAPHeader(w http.ResponseWriter, contentLength int, contentType string) { 122 | setContentType(w, contentType) 123 | w.Header().Set("Content-Length", fmt.Sprint(contentLength)) 124 | } 125 | 126 | func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { 127 | soapAction := r.Header.Get("SOAPAction") 128 | l("ServeHTTP method:", r.Method, ", path:", r.URL.Path, ", SOAPAction", "\""+soapAction+"\"") 129 | // we have a valid request time to call the handler 130 | w = &responseWriter{ 131 | w: w, 132 | outputStarted: false, 133 | } 134 | switch r.Method { 135 | case "POST": 136 | l("incoming POST") 137 | soapRequestBytes, err := ioutil.ReadAll(r.Body) 138 | // Our structs for Envelope, Header, Body and Fault are tagged with namespace for SOAP 1.1 139 | // Therefore we must adjust namespaces for incoming SOAP 1.2 messages 140 | if s.SoapVersion == SoapVersion12 { 141 | tmp := string(soapRequestBytes) 142 | tmp = strings.Replace(tmp, NamespaceSoap12, NamespaceSoap11, -1) 143 | soapRequestBytes = []byte(tmp) 144 | } 145 | 146 | if err != nil { 147 | s.handleError(errors.New("could not read POST:: "+err.Error()), w) 148 | return 149 | } 150 | pathHandlers, pathHandlerOK := s.handlers[r.URL.Path] 151 | if !pathHandlerOK { 152 | s.handleError(errors.New("unknown path"), w) 153 | return 154 | } 155 | actionHandlers, ok := pathHandlers[soapAction] 156 | if !ok { 157 | s.handleError(errors.New("unknown action \""+soapAction+"\""), w) 158 | return 159 | } 160 | 161 | // we need to find out, what is in the body 162 | probeEnvelope := &Envelope{ 163 | Body: Body{ 164 | Content: &dummyContent{}, 165 | }, 166 | } 167 | 168 | err = s.Marshaller.Unmarshal(soapRequestBytes, &probeEnvelope) 169 | if err != nil { 170 | s.handleError(errors.New("could not probe soap body content:: "+err.Error()), w) 171 | return 172 | } 173 | 174 | t := probeEnvelope.Body.SOAPBodyContentType 175 | l("found content type", t) 176 | actionHandler, ok := actionHandlers[t] 177 | if !ok { 178 | s.handleError(errors.New("no action handler for content type: \""+t+"\""), w) 179 | return 180 | } 181 | request := actionHandler.requestFactory() 182 | envelope := &Envelope{ 183 | Header: Header{}, 184 | Body: Body{ 185 | Content: request, 186 | }, 187 | } 188 | 189 | err = xml.Unmarshal(soapRequestBytes, &envelope) 190 | if err != nil { 191 | s.handleError(errors.New("could not unmarshal request:: "+err.Error()), w) 192 | return 193 | } 194 | l("request", jsonDump(envelope)) 195 | 196 | response, err := actionHandler.handler(request, w, r) 197 | if err != nil { 198 | l("action handler threw up") 199 | s.handleError(err, w) 200 | return 201 | } 202 | l("result", jsonDump(response)) 203 | if !w.(*responseWriter).outputStarted { 204 | responseEnvelope := &Envelope{ 205 | Body: Body{ 206 | Content: response, 207 | }, 208 | } 209 | xmlBytes, err := s.Marshaller.Marshal(responseEnvelope) 210 | // Adjust namespaces for SOAP 1.2 211 | if s.SoapVersion == SoapVersion12 { 212 | tmp := string(xmlBytes) 213 | tmp = strings.Replace(tmp, NamespaceSoap11, NamespaceSoap12, -1) 214 | xmlBytes = []byte(tmp) 215 | } 216 | if err != nil { 217 | s.handleError(errors.New("could not marshal response:: "+err.Error()), w) 218 | } 219 | addSOAPHeader(w, len(xmlBytes), s.ContentType) 220 | w.Write(xmlBytes) 221 | } else { 222 | l("action handler sent its own output") 223 | } 224 | 225 | default: 226 | // this will be a soap fault !? 227 | s.handleError(errors.New("this is a soap service - you have to POST soap requests"), w) 228 | } 229 | } 230 | 231 | func jsonDump(v interface{}) string { 232 | if !Verbose { 233 | return "not dumping" 234 | } 235 | jsonBytes, err := json.MarshalIndent(v, "", " ") 236 | if err != nil { 237 | return "error in json dump :: " + err.Error() 238 | } 239 | return string(jsonBytes) 240 | } 241 | 242 | // ListenAndServe run standalone 243 | func (s *Server) ListenAndServe(addr string) error { 244 | return http.ListenAndServe(addr, s) 245 | } 246 | -------------------------------------------------------------------------------- /soap.go: -------------------------------------------------------------------------------- 1 | package soap 2 | 3 | import ( 4 | "encoding/json" 5 | "encoding/xml" 6 | "log" 7 | ) 8 | 9 | // SOAP 1.1 and SOAP 1.2 must expect different ContentTypes and Namespaces. 10 | 11 | const ( 12 | SoapVersion11 = "1.1" 13 | SoapVersion12 = "1.2" 14 | 15 | SoapContentType11 = "text/xml; charset=\"utf-8\"" 16 | SoapContentType12 = "application/soap+xml; charset=\"utf-8\"" 17 | 18 | NamespaceSoap11 = "http://schemas.xmlsoap.org/soap/envelope/" 19 | NamespaceSoap12 = "http://www.w3.org/2003/05/soap-envelope" 20 | ) 21 | 22 | // Verbose be verbose 23 | var Verbose = false 24 | 25 | func l(m ...interface{}) { 26 | if Verbose { 27 | log.Println(m...) 28 | } 29 | } 30 | 31 | func LogJSON(v interface{}) { 32 | if Verbose { 33 | json, err := json.MarshalIndent(v, "", " ") 34 | if err != nil { 35 | log.Println("Could not log json...") 36 | return 37 | } 38 | log.Println(string(json)) 39 | } 40 | } 41 | 42 | // Envelope type 43 | type Envelope struct { 44 | XMLName xml.Name `xml:"http://schemas.xmlsoap.org/soap/envelope/ Envelope"` 45 | Header Header 46 | Body Body 47 | } 48 | 49 | // Header type 50 | type Header struct { 51 | XMLName xml.Name `xml:"http://schemas.xmlsoap.org/soap/envelope/ Header"` 52 | 53 | Header interface{} 54 | } 55 | 56 | // Body type 57 | type Body struct { 58 | XMLName xml.Name `xml:"http://schemas.xmlsoap.org/soap/envelope/ Body"` 59 | 60 | Fault *Fault `xml:",omitempty"` 61 | Content interface{} `xml:",omitempty"` 62 | SOAPBodyContentType string `xml:"-"` 63 | } 64 | 65 | // Fault type 66 | type Fault struct { 67 | XMLName xml.Name `xml:"http://schemas.xmlsoap.org/soap/envelope/ Fault"` 68 | 69 | Code string `xml:"faultcode,omitempty"` 70 | String string `xml:"faultstring,omitempty"` 71 | Actor string `xml:"faultactor,omitempty"` 72 | Detail string `xml:"detail,omitempty"` 73 | } 74 | 75 | // UnmarshalXML implement xml.Unmarshaler 76 | func (b *Body) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { 77 | if b.Content == nil { 78 | return xml.UnmarshalError("Content must be a pointer to a struct") 79 | } 80 | 81 | var ( 82 | token xml.Token 83 | err error 84 | consumed bool 85 | ) 86 | 87 | Loop: 88 | for { 89 | if token, err = d.Token(); err != nil { 90 | return err 91 | } 92 | 93 | if token == nil { 94 | break 95 | } 96 | 97 | switch se := token.(type) { 98 | case xml.StartElement: 99 | if consumed { 100 | return xml.UnmarshalError("Found multiple elements inside SOAP body; not wrapped-document/literal WS-I compliant") 101 | } else if se.Name.Space == "http://schemas.xmlsoap.org/soap/envelope/" && se.Name.Local == "Fault" { 102 | b.Fault = &Fault{} 103 | b.Content = nil 104 | 105 | err = d.DecodeElement(b.Fault, &se) 106 | if err != nil { 107 | return err 108 | } 109 | 110 | consumed = true 111 | } else { 112 | b.SOAPBodyContentType = se.Name.Local 113 | if err = d.DecodeElement(b.Content, &se); err != nil { 114 | return err 115 | } 116 | 117 | consumed = true 118 | } 119 | case xml.EndElement: 120 | break Loop 121 | } 122 | } 123 | 124 | return nil 125 | } 126 | 127 | func (f *Fault) Error() string { 128 | return f.String 129 | } 130 | --------------------------------------------------------------------------------