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