// Package envelope is responsible for encoding and decoding SOAP envelopes. package envelope import ( "encoding/xml" "errors" "fmt" "io" ) // ErrFault can be used as a target with errors.Is. var ErrFault error = errors.New("xml fault") // FaultDetail carries XML-encoded application-specific Fault details. type FaultDetail struct { Raw []byte `xml:",innerxml"` } // Fault implements error, and contains SOAP fault information. type Fault struct { Code string `xml:"faultcode"` String string `xml:"faultstring"` Actor string `xml:"faultactor"` Detail FaultDetail `xml:"detail"` } func (fe *Fault) Error() string { return fmt.Sprintf("SOAP fault code=%s: %s", fe.Code, fe.String) } func (fe *Fault) Is(target error) bool { return target == ErrFault } // Action wraps a SOAP action to be read or written as part of a SOAP envelope. type Action struct { // XMLName specifies the XML element namespace (URI) and name. Together // these identify the SOAP action. XMLName xml.Name // Args is an arbitrary struct containing fields for encoding or decoding // arguments. See https://pkg.go.dev/encoding/xml@go1.17.1#Marshal and // https://pkg.go.dev/encoding/xml@go1.17.1#Unmarshal for details on // annotating fields in the structure. Args any } // NewSendAction creates a SOAP action for receiving arguments. func NewRecvAction(args any) *Action { return &Action{Args: args} } // NewSendAction creates a SOAP action for sending with the given namespace URL, // action name, and arguments. func NewSendAction(serviceType, actionName string, args any) *Action { return &Action{ XMLName: xml.Name{Space: serviceType, Local: actionName}, Args: args, } } var _ xml.Marshaler = &Action{} // MarshalXML implements `xml.Marshaller`. // // This is an implementation detail that allows packing elements inside the // action element from the struct in `a.Args`. func (a *Action) MarshalXML(e *xml.Encoder, start xml.StartElement) error { // Hardcodes the XML namespace. See comment in Write() for context. return e.EncodeElement(a.Args, xml.StartElement{ Name: xml.Name{Space: "", Local: "u:" + a.XMLName.Local}, Attr: []xml.Attr{{ Name: xml.Name{Space: "", Local: "xmlns:u"}, Value: a.XMLName.Space, }}, }) } var _ xml.Unmarshaler = &Action{} // UnmarshalXML implements `xml.Unmarshaller`. // // This is an implementation detail that allows unpacking elements inside the // action element into the struct in `a.Args`. func (a *Action) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { a.XMLName = start.Name return d.DecodeElement(a.Args, &start) } // Various "constant" bytes used in the written envelope. var ( envOpen = []byte(xml.Header + ``) envClose = []byte(``) ) // Write marshals a SOAP envelope to the writer. Errors can be from the writer // or XML encoding. func Write(w io.Writer, action *Action) error { // Experiments with one router have shown that it 500s for requests where // the outer default xmlns is set to the SOAP namespace, and then // reassigning the default namespace within that to the service namespace. // Most of the code in this function is hand-coding the outer XML to // workaround this. // Resolving https://github.com/golang/go/issues/9519 might remove the need // for this workaround. _, err := w.Write(envOpen) if err != nil { return err } enc := xml.NewEncoder(w) err = enc.Encode(action) if err != nil { return err } err = enc.Flush() _, err = w.Write(envClose) return err } // Read unmarshals a SOAP envelope from the reader. Errors can either be from // the reader, XML decoding, or a *Fault. func Read(r io.Reader, action *Action) error { env := envelope{ Body: body{ Action: action, }, } dec := xml.NewDecoder(r) err := dec.Decode(&env) if err != nil { return err } if env.Body.Fault != nil { return env.Body.Fault } return nil } type envelope struct { XMLName xml.Name `xml:"http://schemas.xmlsoap.org/soap/envelope/ Envelope"` EncodingStyle string `xml:"http://schemas.xmlsoap.org/soap/envelope/ encodingStyle,attr"` Body body `xml:"http://schemas.xmlsoap.org/soap/envelope/ Body"` } type body struct { Fault *Fault `xml:"Fault"` Action *Action `xml:",any"` }