connector.go raw
1 package ibclient
2
3 import (
4 "bytes"
5 "crypto/tls"
6 "crypto/x509"
7 "encoding/json"
8 "errors"
9 "fmt"
10 "io/ioutil"
11 "log"
12 "net/http"
13 "net/http/cookiejar"
14 "net/url"
15 "reflect"
16 "strings"
17 "time"
18
19 "golang.org/x/net/publicsuffix"
20 )
21
22 type AuthConfig struct {
23 Username string
24 Password string
25
26 ClientCert []byte
27 ClientKey []byte
28 }
29
30 type HostConfig struct {
31 Scheme string
32 Host string
33 Version string
34 Port string
35 }
36
37 type TransportConfig struct {
38 SslVerify bool
39 certPool *x509.CertPool
40 HttpRequestTimeout time.Duration // in seconds
41 HttpPoolConnections int
42 ProxyUrl *url.URL
43 }
44
45 func NewTransportConfig(sslVerify string, httpRequestTimeout int, httpPoolConnections int) (cfg TransportConfig) {
46 switch {
47 case "false" == strings.ToLower(sslVerify):
48 cfg.SslVerify = false
49 case "true" == strings.ToLower(sslVerify):
50 cfg.SslVerify = true
51 default:
52 caPool := x509.NewCertPool()
53 cert, err := ioutil.ReadFile(sslVerify)
54 if err != nil {
55 log.Printf("Cannot load certificate file '%s'", sslVerify)
56 return
57 }
58 if !caPool.AppendCertsFromPEM(cert) {
59 err = fmt.Errorf("cannot append certificate from file '%s'", sslVerify)
60 return
61 }
62 cfg.certPool = caPool
63 cfg.SslVerify = true
64 }
65
66 cfg.HttpPoolConnections = httpPoolConnections
67 cfg.HttpRequestTimeout = time.Duration(httpRequestTimeout)
68
69 return
70 }
71
72 type HttpRequestBuilder interface {
73 Init(HostConfig, AuthConfig)
74 BuildUrl(r RequestType, objType string, ref string, returnFields []string, queryParams *QueryParams) (urlStr string)
75 BuildBody(r RequestType, obj IBObject) (jsonStr []byte)
76 BuildRequest(r RequestType, obj IBObject, ref string, queryParams *QueryParams) (req *http.Request, err error)
77 }
78
79 type HttpRequestor interface {
80 Init(AuthConfig, TransportConfig)
81 SendRequest(*http.Request) ([]byte, error)
82 }
83
84 type WapiRequestBuilder struct {
85 hostCfg HostConfig
86 authCfg AuthConfig
87 }
88
89 type WapiRequestBuilderWithHeaders struct {
90 HttpRequestBuilder
91 header http.Header
92 }
93
94 func NewWapiRequestBuilderWithHeaders(wrb *WapiRequestBuilder, header http.Header) (*WapiRequestBuilderWithHeaders, error) {
95 return &WapiRequestBuilderWithHeaders{
96 HttpRequestBuilder: wrb,
97 header: header,
98 }, nil
99 }
100
101 func (wrbh *WapiRequestBuilderWithHeaders) BuildRequest(r RequestType, obj IBObject, ref string, queryParams *QueryParams) (req *http.Request, err error) {
102 req, err = wrbh.HttpRequestBuilder.BuildRequest(r, obj, ref, queryParams)
103 if err != nil {
104 return req, err
105 }
106 for h, values := range wrbh.header {
107 for _, v := range values {
108 req.Header.Add(h, v)
109 }
110 }
111 return req, nil
112 }
113
114 type WapiHttpRequestor struct {
115 client http.Client
116 }
117
118 type IBConnector interface {
119 CreateObject(obj IBObject) (ref string, err error)
120 GetObject(obj IBObject, ref string, queryParams *QueryParams, res interface{}) error
121 DeleteObject(ref string) (refRes string, err error)
122 UpdateObject(obj IBObject, ref string) (refRes string, err error)
123 }
124
125 type Connector struct {
126 hostCfg HostConfig
127 authCfg AuthConfig
128 transportCfg TransportConfig
129 requestBuilder HttpRequestBuilder
130 requestor HttpRequestor
131 }
132
133 type RequestType int
134
135 const (
136 CREATE RequestType = iota
137 GET
138 DELETE
139 UPDATE
140 )
141
142 func (r RequestType) toMethod() string {
143 switch r {
144 case CREATE:
145 return "POST"
146 case GET:
147 return "GET"
148 case DELETE:
149 return "DELETE"
150 case UPDATE:
151 return "PUT"
152 }
153
154 return ""
155 }
156
157 func getHTTPResponseError(resp *http.Response) error {
158 defer resp.Body.Close()
159 content, _ := ioutil.ReadAll(resp.Body)
160 msg := fmt.Sprintf("WAPI request error: %d('%s')\nContents:\n%s\n", resp.StatusCode, resp.Status, content)
161 if resp.StatusCode == http.StatusNotFound {
162 return NewNotFoundError(msg)
163 }
164 return errors.New(msg)
165 }
166
167 func (whr *WapiHttpRequestor) Init(authCfg AuthConfig, trCfg TransportConfig) {
168 var certList []tls.Certificate
169
170 clientAuthType := tls.NoClientCert
171
172 if authCfg.ClientKey != nil && authCfg.ClientCert != nil {
173 cert, err := tls.X509KeyPair(authCfg.ClientCert, authCfg.ClientKey)
174 if err != nil {
175 log.Fatal("Invalid certificate key pair (PEM format error): ", err)
176 }
177
178 certList = []tls.Certificate{cert}
179 clientAuthType = tls.RequestClientCert
180 }
181
182 tr := &http.Transport{
183 TLSClientConfig: &tls.Config{
184 RootCAs: trCfg.certPool,
185 ClientAuth: clientAuthType,
186 Certificates: certList,
187 InsecureSkipVerify: !trCfg.SslVerify,
188 Renegotiation: tls.RenegotiateOnceAsClient,
189 },
190 MaxIdleConnsPerHost: trCfg.HttpPoolConnections,
191 Proxy: http.ProxyFromEnvironment,
192 }
193
194 if trCfg.ProxyUrl != nil {
195 tr.Proxy = http.ProxyURL(trCfg.ProxyUrl)
196 }
197
198 // All users of cookiejar should import "golang.org/x/net/publicsuffix"
199 jar, err := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List})
200 if err != nil {
201 log.Fatal(err)
202 }
203
204 whr.client = http.Client{
205 Jar: jar,
206 Transport: tr,
207 Timeout: trCfg.HttpRequestTimeout * time.Second,
208 }
209 }
210
211 func (whr *WapiHttpRequestor) SendRequest(req *http.Request) (res []byte, err error) {
212 var resp *http.Response
213 resp, err = whr.client.Do(req)
214 if err != nil {
215 return
216 } else if !(resp.StatusCode == http.StatusOK ||
217 (resp.StatusCode == http.StatusCreated &&
218 req.Method == CREATE.toMethod())) {
219 err := getHTTPResponseError(resp)
220 return nil, err
221 }
222 defer resp.Body.Close()
223 res, err = ioutil.ReadAll(resp.Body)
224 if err != nil {
225 log.Printf("Http Reponse ioutil.ReadAll() Error: '%s'", err)
226 return
227 }
228
229 return
230 }
231
232 func NewWapiRequestBuilder(hostCfg HostConfig, authCfg AuthConfig) (*WapiRequestBuilder, error) {
233 wrb := WapiRequestBuilder{
234 hostCfg: hostCfg,
235 authCfg: authCfg,
236 }
237
238 return &wrb, nil
239 }
240
241 func (wrb *WapiRequestBuilder) Init(hostCfg HostConfig, authCfg AuthConfig) {
242 wrb.hostCfg = hostCfg
243 wrb.authCfg = authCfg
244 }
245
246 func (wrb *WapiRequestBuilder) BuildUrl(t RequestType, objType string, ref string, returnFields []string, queryParams *QueryParams) (urlStr string) {
247 path := []string{"wapi", "v" + wrb.hostCfg.Version}
248 if len(ref) > 0 {
249 path = append(path, ref)
250 } else {
251 path = append(path, objType)
252 }
253
254 qry := ""
255 vals := url.Values{}
256 if t == GET {
257 if len(returnFields) > 0 {
258 vals.Set("_return_fields", strings.Join(returnFields, ","))
259 }
260 if queryParams != nil {
261 // TODO need to get this from individual objects in future
262 if queryParams.forceProxy {
263 vals.Set("_proxy_search", "GM")
264 }
265 for k, v := range queryParams.searchFields {
266 if res, ok := ValidateMultiValue(v); ok {
267 for _, mv := range res {
268 vals.Add(k, strings.TrimSpace(mv))
269 }
270 } else {
271 vals.Set(k, v)
272 }
273 }
274 }
275
276 qry = vals.Encode()
277 }
278
279 scheme := "https"
280 if wrb.hostCfg.Scheme == "http" {
281 scheme = "http"
282 }
283 u := url.URL{
284 Scheme: scheme,
285 Host: wrb.hostCfg.Host + ":" + wrb.hostCfg.Port,
286 Path: strings.Join(path, "/"),
287 RawQuery: qry,
288 }
289
290 return u.String()
291 }
292
293 // populateNilLists fills nil lists in IBObject structs, since NIOS doesn't accept a null list in JSON payload.
294 func (wrb *WapiRequestBuilder) populateNilLists(obj IBObject) IBObject {
295 objVal := reflect.ValueOf(obj)
296 if reflect.ValueOf(obj).Kind() == reflect.Ptr {
297 objVal = objVal.Elem()
298 }
299
300 for i := 0; i < objVal.NumField(); i++ {
301 fieldVal := objVal.Field(i)
302 if fieldVal.Type().Kind() == reflect.Slice && fieldVal.CanSet() {
303 if fieldVal.IsNil() == true {
304 fieldVal.Set(reflect.MakeSlice(fieldVal.Type(), 0, 0))
305 }
306 }
307 }
308
309 return obj
310 }
311
312 func (wrb *WapiRequestBuilder) BuildBody(t RequestType, obj IBObject) []byte {
313 var objJSON []byte
314 var err error
315
316 obj = wrb.populateNilLists(obj)
317
318 objJSON, err = json.Marshal(obj)
319 if err != nil {
320 log.Printf("Cannot marshal object '%s': %s", obj, err)
321 return nil
322 }
323
324 eaSearch := obj.EaSearch()
325 if t == GET && len(eaSearch) > 0 {
326 eaSearchJSON, err := json.Marshal(eaSearch)
327 if err != nil {
328 log.Printf("Cannot marshal EA Search attributes. '%s'\n", err)
329 return nil
330 }
331 objJSON = append(append(objJSON[:len(objJSON)-1], byte(',')), eaSearchJSON[1:]...)
332 }
333
334 return objJSON
335 }
336
337 func (wrb *WapiRequestBuilder) BuildRequest(t RequestType, obj IBObject, ref string, queryParams *QueryParams) (req *http.Request, err error) {
338 var (
339 objType string
340 returnFields []string
341 )
342 if obj != nil {
343 objType = obj.ObjectType()
344 returnFields = obj.ReturnFields()
345 }
346 urlStr := wrb.BuildUrl(t, objType, ref, returnFields, queryParams)
347
348 var bodyStr []byte
349 if obj != nil && (t == CREATE || t == UPDATE) {
350 bodyStr = wrb.BuildBody(t, obj)
351 }
352
353 req, err = http.NewRequest(t.toMethod(), urlStr, bytes.NewBuffer(bodyStr))
354 if err != nil {
355 log.Printf("cannot build request: '%s'", err)
356 return
357 }
358 req.Header.Set("Content-Type", "application/json")
359 if wrb.authCfg.Username != "" {
360 req.SetBasicAuth(wrb.authCfg.Username, wrb.authCfg.Password)
361 }
362
363 return
364 }
365
366 func (c *Connector) makeRequest(t RequestType, obj IBObject, ref string, queryParams *QueryParams) (res []byte, err error) {
367 var req *http.Request
368 req, err = c.requestBuilder.BuildRequest(t, obj, ref, queryParams)
369 if err != nil {
370 return
371 }
372 res, err = c.requestor.SendRequest(req)
373 if err != nil {
374 if queryParams != nil {
375 /* Forcing the request to redirect to Grid Master by making forcedProxy=true */
376 queryParams.forceProxy = true
377 req, err = c.requestBuilder.BuildRequest(t, obj, ref, queryParams)
378 if err != nil {
379 return
380 }
381 res, err = c.requestor.SendRequest(req)
382 } else {
383 return nil, err
384 }
385 }
386
387 return
388 }
389
390 func (c *Connector) CreateObject(obj IBObject) (ref string, err error) {
391 ref = ""
392 queryParams := NewQueryParams(false, nil)
393 resp, err := c.makeRequest(CREATE, obj, "", queryParams)
394 if err != nil || len(resp) == 0 {
395 log.Printf("CreateObject request error: '%s'\n", err)
396 return
397 }
398
399 err = json.Unmarshal(resp, &ref)
400 if err != nil {
401 log.Printf("cannot unmarshall '%s', err: '%s'\n", string(resp), err)
402 return
403 }
404
405 return
406 }
407
408 func (c *Connector) GetObject(
409 obj IBObject, ref string,
410 queryParams *QueryParams, res interface{}) (err error) {
411
412 resp, err := c.makeRequest(GET, obj, ref, queryParams)
413 if err != nil {
414 return
415 }
416 //to check empty underlying value of interface
417 var result interface{}
418 err = json.Unmarshal(resp, &result)
419 if err != nil {
420 log.Printf("cannot unmarshall to check empty value '%s': '%s'\n", string(resp), err)
421 }
422
423 var data []interface{}
424 if resp == nil || (reflect.TypeOf(result) == reflect.TypeOf(data) && len(result.([]interface{})) == 0) {
425 if queryParams == nil {
426 err = NewNotFoundError("requested object not found")
427 return
428 }
429 queryParams.forceProxy = true
430 resp, err = c.makeRequest(GET, obj, ref, queryParams)
431 }
432 if err != nil {
433 log.Printf("GetObject request error: '%s'\n", err)
434 }
435 err = json.Unmarshal(resp, res)
436 if err != nil {
437 log.Printf("cannot unmarshall '%s', err: '%s'\n", string(resp), err)
438 return
439 }
440
441 if string(resp) == "[]" {
442 return NewNotFoundError("not found")
443 }
444
445 return
446 }
447
448 func (c *Connector) DeleteObject(ref string) (refRes string, err error) {
449 refRes = ""
450 queryParams := NewQueryParams(false, nil)
451 resp, err := c.makeRequest(DELETE, nil, ref, queryParams)
452 if err != nil {
453 log.Printf("DeleteObject request error: '%s'\n", err)
454 return
455 }
456
457 err = json.Unmarshal(resp, &refRes)
458 if err != nil {
459 log.Printf("cannot unmarshall '%s': '%s'\n", string(resp), err)
460 return
461 }
462
463 return
464 }
465
466 func (c *Connector) UpdateObject(obj IBObject, ref string) (refRes string, err error) {
467 queryParams := NewQueryParams(false, nil)
468 refRes = ""
469 resp, err := c.makeRequest(UPDATE, obj, ref, queryParams)
470 if err != nil {
471 log.Printf("failed to update object %s: %s", obj.ObjectType(), err)
472 return
473 }
474
475 err = json.Unmarshal(resp, &refRes)
476 if err != nil {
477 log.Printf("cannot unmarshall update object response'%s', err: '%s'\n", string(resp), err)
478 return
479 }
480 return
481 }
482
483 // Logout sends a request to invalidate the ibapauth cookie and should
484 // be used in a defer statement after the Connector has been successfully
485 // initialized.
486 func (c *Connector) Logout() (err error) {
487 queryParams := NewQueryParams(false, nil)
488 _, err = c.makeRequest(CREATE, nil, "logout", queryParams)
489 if err != nil {
490 log.Printf("Logout request error: '%s'\n", err)
491 }
492
493 return
494 }
495
496 var ValidateConnector = validateConnector
497
498 func validateConnector(c *Connector) (err error) {
499 // GET UserProfile request is used here to validate connector's basic auth and reachability.
500 // TODO: It seems to be broken, needs to be fixed.
501 //var response []UserProfile
502 //userprofile := NewUserProfile(UserProfile{})
503 //err = c.GetObject(userprofile, "", &response)
504 //if err != nil {
505 // log.Printf("Failed to connect to the Grid, err: %s \n", err)
506 //}
507 return
508 }
509
510 func NewConnector(hostConfig HostConfig, authCfg AuthConfig, transportConfig TransportConfig,
511 requestBuilder HttpRequestBuilder, requestor HttpRequestor) (res *Connector, err error) {
512 res = nil
513
514 connector := &Connector{
515 hostCfg: hostConfig,
516 authCfg: authCfg,
517 transportCfg: transportConfig,
518 }
519
520 //connector.requestBuilder = WapiRequestBuilder{WaipHostConfig: connector.hostCfg}
521 connector.requestBuilder = requestBuilder
522 connector.requestBuilder.Init(connector.hostCfg, connector.authCfg)
523
524 connector.requestor = requestor
525 connector.requestor.Init(connector.authCfg, connector.transportCfg)
526
527 res = connector
528 err = ValidateConnector(connector)
529 return
530 }
531