comparison pkg/soap/soap.go @ 566:5e45f441aed6

Added SOAP client for the IFBN bottleneck service. Generated by github.com/hooklift/gowsdl from official WSDL (2018-09-03) and edit by hand to resolve some problems.
author Sascha L. Teichmann <sascha.teichmann@intevation.de>
date Tue, 04 Sep 2018 17:13:25 +0200
parents
children a244b18cb916
comparison
equal deleted inserted replaced
565:4bc27eea4f09 566:5e45f441aed6
1 package soap
2
3 import (
4 "bytes"
5 "crypto/tls"
6 "encoding/xml"
7 "io/ioutil"
8 "log"
9 "math/rand"
10 "net"
11 "net/http"
12 "time"
13 )
14
15 const timeout = time.Duration(30 * time.Second)
16
17 func dialTimeout(network, addr string) (net.Conn, error) {
18 return net.DialTimeout(network, addr, timeout)
19 }
20
21 type SOAPEnvelope struct {
22 XMLName xml.Name `xml:"http://schemas.xmlsoap.org/soap/envelope/ Envelope"`
23 Header *SOAPHeader
24 Body SOAPBody
25 }
26
27 type SOAPHeader struct {
28 XMLName xml.Name `xml:"http://schemas.xmlsoap.org/soap/envelope/ Header"`
29
30 Items []interface{} `xml:",omitempty"`
31 }
32
33 type SOAPBody struct {
34 XMLName xml.Name `xml:"http://schemas.xmlsoap.org/soap/envelope/ Body"`
35
36 Fault *SOAPFault `xml:",omitempty"`
37 Content interface{} `xml:",omitempty"`
38 }
39
40 type SOAPFault struct {
41 XMLName xml.Name `xml:"http://schemas.xmlsoap.org/soap/envelope/ Fault"`
42
43 Code string `xml:"faultcode,omitempty"`
44 String string `xml:"faultstring,omitempty"`
45 Actor string `xml:"faultactor,omitempty"`
46 Detail string `xml:"detail,omitempty"`
47 }
48
49 const (
50 // Predefined WSS namespaces to be used in
51 WssNsWSSE string = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"
52 WssNsWSU string = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"
53 WssNsType string = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText"
54 )
55
56 type WSSSecurityHeader struct {
57 XMLName xml.Name `xml:"http://schemas.xmlsoap.org/soap/envelope/ wsse:Security"`
58 XmlNSWsse string `xml:"xmlns:wsse,attr"`
59
60 MustUnderstand string `xml:"mustUnderstand,attr,omitempty"`
61
62 Token *WSSUsernameToken `xml:",omitempty"`
63 }
64
65 type WSSUsernameToken struct {
66 XMLName xml.Name `xml:"wsse:UsernameToken"`
67 XmlNSWsu string `xml:"xmlns:wsu,attr"`
68 XmlNSWsse string `xml:"xmlns:wsse,attr"`
69
70 Id string `xml:"wsu:Id,attr,omitempty"`
71
72 Username *WSSUsername `xml:",omitempty"`
73 Password *WSSPassword `xml:",omitempty"`
74 }
75
76 type WSSUsername struct {
77 XMLName xml.Name `xml:"wsse:Username"`
78 XmlNSWsse string `xml:"xmlns:wsse,attr"`
79
80 Data string `xml:",chardata"`
81 }
82
83 type WSSPassword struct {
84 XMLName xml.Name `xml:"wsse:Password"`
85 XmlNSWsse string `xml:"xmlns:wsse,attr"`
86 XmlNSType string `xml:"Type,attr"`
87
88 Data string `xml:",chardata"`
89 }
90
91 type BasicAuth struct {
92 Login string
93 Password string
94 }
95
96 type SOAPClient struct {
97 url string
98 tlsCfg *tls.Config
99 auth *BasicAuth
100 headers []interface{}
101 }
102
103 // **********
104 // Accepted solution from http://stackoverflow.com/questions/22892120/how-to-generate-a-random-string-of-a-fixed-length-in-golang
105 // Author: Icza - http://stackoverflow.com/users/1705598/icza
106
107 const (
108 letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
109 letterIdxBits = 6 // 6 bits to represent a letter index
110 letterIdxMask = 1<<letterIdxBits - 1 // All 1-bits, as many as letterIdxBits
111 letterIdxMax = 63 / letterIdxBits // # of letter indices fitting in 63 bits
112 )
113
114 func randStringBytesMaskImprSrc(n int) string {
115 src := rand.NewSource(time.Now().UnixNano())
116 b := make([]byte, n)
117 // A src.Int63() generates 63 random bits, enough for letterIdxMax characters!
118 for i, cache, remain := n-1, src.Int63(), letterIdxMax; i >= 0; {
119 if remain == 0 {
120 cache, remain = src.Int63(), letterIdxMax
121 }
122 if idx := int(cache & letterIdxMask); idx < len(letterBytes) {
123 b[i] = letterBytes[idx]
124 i--
125 }
126 cache >>= letterIdxBits
127 remain--
128 }
129 return string(b)
130 }
131
132 // **********
133
134 func NewWSSSecurityHeader(user, pass, mustUnderstand string) *WSSSecurityHeader {
135 hdr := &WSSSecurityHeader{XmlNSWsse: WssNsWSSE, MustUnderstand: mustUnderstand}
136 hdr.Token = &WSSUsernameToken{XmlNSWsu: WssNsWSU, XmlNSWsse: WssNsWSSE, Id: "UsernameToken-" + randStringBytesMaskImprSrc(9)}
137 hdr.Token.Username = &WSSUsername{XmlNSWsse: WssNsWSSE, Data: user}
138 hdr.Token.Password = &WSSPassword{XmlNSWsse: WssNsWSSE, XmlNSType: WssNsType, Data: pass}
139 return hdr
140 }
141
142 func (b *SOAPBody) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
143 if b.Content == nil {
144 return xml.UnmarshalError("Content must be a pointer to a struct")
145 }
146
147 var (
148 token xml.Token
149 err error
150 consumed bool
151 )
152
153 Loop:
154 for {
155 if token, err = d.Token(); err != nil {
156 return err
157 }
158
159 if token == nil {
160 break
161 }
162
163 switch se := token.(type) {
164 case xml.StartElement:
165 if consumed {
166 return xml.UnmarshalError("Found multiple elements inside SOAP body; not wrapped-document/literal WS-I compliant")
167 } else if se.Name.Space == "http://schemas.xmlsoap.org/soap/envelope/" && se.Name.Local == "Fault" {
168 b.Fault = &SOAPFault{}
169 b.Content = nil
170
171 err = d.DecodeElement(b.Fault, &se)
172 if err != nil {
173 return err
174 }
175
176 consumed = true
177 } else {
178 if err = d.DecodeElement(b.Content, &se); err != nil {
179 return err
180 }
181
182 consumed = true
183 }
184 case xml.EndElement:
185 break Loop
186 }
187 }
188
189 return nil
190 }
191
192 func (f *SOAPFault) Error() string {
193 return f.String
194 }
195
196 func NewSOAPClient(url string, insecureSkipVerify bool, auth *BasicAuth) *SOAPClient {
197 tlsCfg := &tls.Config{
198 InsecureSkipVerify: insecureSkipVerify,
199 }
200 return NewSOAPClientWithTLSConfig(url, tlsCfg, auth)
201 }
202
203 func NewSOAPClientWithTLSConfig(url string, tlsCfg *tls.Config, auth *BasicAuth) *SOAPClient {
204 return &SOAPClient{
205 url: url,
206 tlsCfg: tlsCfg,
207 auth: auth,
208 }
209 }
210
211 func (s *SOAPClient) AddHeader(header interface{}) {
212 s.headers = append(s.headers, header)
213 }
214
215 func (s *SOAPClient) Call(soapAction string, request, response interface{}) error {
216 envelope := SOAPEnvelope{}
217
218 if s.headers != nil && len(s.headers) > 0 {
219 soapHeader := &SOAPHeader{Items: make([]interface{}, len(s.headers))}
220 copy(soapHeader.Items, s.headers)
221 envelope.Header = soapHeader
222 }
223
224 envelope.Body.Content = request
225 buffer := new(bytes.Buffer)
226
227 encoder := xml.NewEncoder(buffer)
228 //encoder.Indent("", " ")
229
230 if err := encoder.Encode(envelope); err != nil {
231 return err
232 }
233
234 if err := encoder.Flush(); err != nil {
235 return err
236 }
237
238 //log.Println(buffer.String())
239
240 req, err := http.NewRequest("POST", s.url, buffer)
241 if err != nil {
242 return err
243 }
244 if s.auth != nil {
245 req.SetBasicAuth(s.auth.Login, s.auth.Password)
246 }
247
248 req.Header.Add("Content-Type", "text/xml; charset=\"utf-8\"")
249 req.Header.Add("SOAPAction", soapAction)
250
251 req.Header.Set("User-Agent", "gowsdl/0.1")
252 req.Close = true
253
254 tr := &http.Transport{
255 TLSClientConfig: s.tlsCfg,
256 Dial: dialTimeout,
257 }
258
259 client := &http.Client{Transport: tr}
260 res, err := client.Do(req)
261 if err != nil {
262 return err
263 }
264 defer res.Body.Close()
265
266 rawbody, err := ioutil.ReadAll(res.Body)
267 if err != nil {
268 return err
269 }
270 if len(rawbody) == 0 {
271 log.Println("empty response")
272 return nil
273 }
274
275 //log.Println(string(rawbody))
276 respEnvelope := new(SOAPEnvelope)
277 respEnvelope.Body = SOAPBody{Content: response}
278 err = xml.Unmarshal(rawbody, respEnvelope)
279 if err != nil {
280 return err
281 }
282
283 fault := respEnvelope.Body.Fault
284 if fault != nil {
285 return fault
286 }
287
288 return nil
289 }