github.com/rakutentech/cli@v6.12.5-0.20151006231303-24468b65536e+incompatible/cf/net/gateway.go (about)

     1  package net
     2  
     3  import (
     4  	"bytes"
     5  	"crypto/tls"
     6  	"encoding/json"
     7  	"fmt"
     8  	"io"
     9  	"io/ioutil"
    10  	"net"
    11  	"net/http"
    12  	"net/url"
    13  	"os"
    14  	"runtime"
    15  	"strings"
    16  	"time"
    17  
    18  	"github.com/cloudfoundry/cli/cf"
    19  	"github.com/cloudfoundry/cli/cf/configuration/core_config"
    20  	"github.com/cloudfoundry/cli/cf/errors"
    21  	. "github.com/cloudfoundry/cli/cf/i18n"
    22  	"github.com/cloudfoundry/cli/cf/terminal"
    23  )
    24  
    25  const (
    26  	JOB_FINISHED             = "finished"
    27  	JOB_FAILED               = "failed"
    28  	DEFAULT_POLLING_THROTTLE = 5 * time.Second
    29  )
    30  
    31  type JobResource struct {
    32  	Entity struct {
    33  		Status       string
    34  		ErrorDetails struct {
    35  			Description string
    36  		} `json:"error_details"`
    37  	}
    38  }
    39  
    40  type AsyncResource struct {
    41  	Metadata struct {
    42  		URL string
    43  	}
    44  }
    45  
    46  type apiErrorHandler func(statusCode int, body []byte) error
    47  
    48  type tokenRefresher interface {
    49  	RefreshAuthToken() (string, error)
    50  }
    51  
    52  type Request struct {
    53  	HttpReq      *http.Request
    54  	SeekableBody io.ReadSeeker
    55  }
    56  
    57  type Gateway struct {
    58  	authenticator   tokenRefresher
    59  	errHandler      apiErrorHandler
    60  	PollingEnabled  bool
    61  	PollingThrottle time.Duration
    62  	trustedCerts    []tls.Certificate
    63  	config          core_config.Reader
    64  	warnings        *[]string
    65  	Clock           func() time.Time
    66  	transport       *http.Transport
    67  	ui              terminal.UI
    68  }
    69  
    70  func newGateway(errHandler apiErrorHandler, config core_config.Reader, ui terminal.UI) (gateway Gateway) {
    71  	gateway.errHandler = errHandler
    72  	gateway.config = config
    73  	gateway.PollingThrottle = DEFAULT_POLLING_THROTTLE
    74  	gateway.warnings = &[]string{}
    75  	gateway.Clock = time.Now
    76  	gateway.ui = ui
    77  
    78  	return
    79  }
    80  
    81  func (gateway *Gateway) AsyncTimeout() time.Duration {
    82  	if gateway.config.AsyncTimeout() > 0 {
    83  		return time.Duration(gateway.config.AsyncTimeout()) * time.Minute
    84  	}
    85  
    86  	return 0
    87  }
    88  
    89  func (gateway *Gateway) SetTokenRefresher(auth tokenRefresher) {
    90  	gateway.authenticator = auth
    91  }
    92  
    93  func (gateway Gateway) GetResource(url string, resource interface{}) (err error) {
    94  	request, err := gateway.NewRequest("GET", url, gateway.config.AccessToken(), nil)
    95  	if err != nil {
    96  		return
    97  	}
    98  
    99  	_, err = gateway.PerformRequestForJSONResponse(request, resource)
   100  	return
   101  }
   102  
   103  func (gateway Gateway) CreateResourceFromStruct(endpoint, url string, resource interface{}) error {
   104  	bytes, err := json.Marshal(resource)
   105  	if err != nil {
   106  		return err
   107  	}
   108  
   109  	return gateway.CreateResource(endpoint, url, strings.NewReader(string(bytes)))
   110  }
   111  
   112  func (gateway Gateway) UpdateResourceFromStruct(endpoint, apiUrl string, resource interface{}) error {
   113  	bytes, err := json.Marshal(resource)
   114  	if err != nil {
   115  		return err
   116  	}
   117  
   118  	return gateway.UpdateResource(endpoint, apiUrl, strings.NewReader(string(bytes)))
   119  }
   120  
   121  func (gateway Gateway) CreateResource(endpoint, apiUrl string, body io.ReadSeeker, resource ...interface{}) (apiErr error) {
   122  	return gateway.createUpdateOrDeleteResource("POST", endpoint, apiUrl, body, false, resource...)
   123  }
   124  
   125  func (gateway Gateway) UpdateResource(endpoint, apiUrl string, body io.ReadSeeker, resource ...interface{}) (apiErr error) {
   126  	return gateway.createUpdateOrDeleteResource("PUT", endpoint, apiUrl, body, false, resource...)
   127  }
   128  
   129  func (gateway Gateway) UpdateResourceSync(endpoint, apiUrl string, body io.ReadSeeker, resource ...interface{}) (apiErr error) {
   130  	return gateway.createUpdateOrDeleteResource("PUT", endpoint, apiUrl, body, true, resource...)
   131  }
   132  
   133  func (gateway Gateway) DeleteResourceSynchronously(endpoint, apiUrl string) (apiErr error) {
   134  	return gateway.createUpdateOrDeleteResource("DELETE", endpoint, apiUrl, nil, true, &AsyncResource{})
   135  }
   136  
   137  func (gateway Gateway) DeleteResource(endpoint, apiUrl string) (apiErr error) {
   138  	return gateway.createUpdateOrDeleteResource("DELETE", endpoint, apiUrl, nil, false, &AsyncResource{})
   139  }
   140  
   141  func (gateway Gateway) ListPaginatedResources(target string,
   142  	path string,
   143  	resource interface{},
   144  	cb func(interface{}) bool) (apiErr error) {
   145  	for path != "" {
   146  		pagination := NewPaginatedResources(resource)
   147  
   148  		apiErr = gateway.GetResource(fmt.Sprintf("%s%s", target, path), &pagination)
   149  		if apiErr != nil {
   150  			return
   151  		}
   152  
   153  		resources, err := pagination.Resources()
   154  		if err != nil {
   155  			return errors.NewWithError(T("Error parsing JSON"), err)
   156  		}
   157  
   158  		for _, resource := range resources {
   159  			if !cb(resource) {
   160  				return
   161  			}
   162  		}
   163  
   164  		path = pagination.NextURL
   165  	}
   166  
   167  	return
   168  }
   169  
   170  func (gateway Gateway) createUpdateOrDeleteResource(verb, endpoint, apiUrl string, body io.ReadSeeker, sync bool, optionalResource ...interface{}) (apiErr error) {
   171  	var resource interface{}
   172  	if len(optionalResource) > 0 {
   173  		resource = optionalResource[0]
   174  	}
   175  
   176  	request, apiErr := gateway.NewRequest(verb, endpoint+apiUrl, gateway.config.AccessToken(), body)
   177  	if apiErr != nil {
   178  		return
   179  	}
   180  
   181  	if resource == nil {
   182  		_, apiErr = gateway.PerformRequest(request)
   183  		return
   184  	}
   185  
   186  	if gateway.PollingEnabled && !sync {
   187  		_, apiErr = gateway.PerformPollingRequestForJSONResponse(endpoint, request, resource, gateway.AsyncTimeout())
   188  		return
   189  	} else {
   190  		_, apiErr = gateway.PerformRequestForJSONResponse(request, resource)
   191  		return
   192  	}
   193  
   194  }
   195  
   196  func (gateway Gateway) newRequest(request *http.Request, accessToken string, body io.ReadSeeker) (*Request, error) {
   197  	if accessToken != "" {
   198  		request.Header.Set("Authorization", accessToken)
   199  	}
   200  
   201  	request.Header.Set("accept", "application/json")
   202  	request.Header.Set("content-type", "application/json")
   203  	request.Header.Set("User-Agent", "go-cli "+cf.Version+" / "+runtime.GOOS)
   204  	return &Request{HttpReq: request, SeekableBody: body}, nil
   205  }
   206  
   207  func (gateway Gateway) NewRequestForFile(method, fullUrl, accessToken string, body *os.File) (req *Request, apiErr error) {
   208  	progressReader := NewProgressReader(body, gateway.ui, 5*time.Second)
   209  	progressReader.Seek(0, 0)
   210  	fileStats, err := body.Stat()
   211  
   212  	if err != nil {
   213  		apiErr = errors.NewWithError(T("Error getting file info"), err)
   214  		return
   215  	}
   216  
   217  	request, err := http.NewRequest(method, fullUrl, progressReader)
   218  	if err != nil {
   219  		apiErr = errors.NewWithError(T("Error building request"), err)
   220  		return
   221  	}
   222  
   223  	fileSize := fileStats.Size()
   224  	progressReader.SetTotalSize(fileSize)
   225  	request.ContentLength = fileSize
   226  
   227  	if err != nil {
   228  		apiErr = errors.NewWithError(T("Error building request"), err)
   229  		return
   230  	}
   231  
   232  	return gateway.newRequest(request, accessToken, progressReader)
   233  }
   234  
   235  func (gateway Gateway) NewRequest(method, path, accessToken string, body io.ReadSeeker) (req *Request, apiErr error) {
   236  	request, err := http.NewRequest(method, path, body)
   237  	if err != nil {
   238  		apiErr = errors.NewWithError(T("Error building request"), err)
   239  		return
   240  	}
   241  	return gateway.newRequest(request, accessToken, body)
   242  }
   243  
   244  func (gateway Gateway) PerformRequest(request *Request) (rawResponse *http.Response, apiErr error) {
   245  	return gateway.doRequestHandlingAuth(request)
   246  }
   247  
   248  func (gateway Gateway) performRequestForResponseBytes(request *Request) (bytes []byte, headers http.Header, rawResponse *http.Response, apiErr error) {
   249  	rawResponse, apiErr = gateway.doRequestHandlingAuth(request)
   250  	if apiErr != nil {
   251  		return
   252  	}
   253  	defer rawResponse.Body.Close()
   254  
   255  	bytes, err := ioutil.ReadAll(rawResponse.Body)
   256  	if err != nil {
   257  		apiErr = errors.NewWithError(T("Error reading response"), err)
   258  	}
   259  
   260  	headers = rawResponse.Header
   261  	return
   262  }
   263  
   264  func (gateway Gateway) PerformRequestForTextResponse(request *Request) (response string, headers http.Header, apiErr error) {
   265  	bytes, headers, _, apiErr := gateway.performRequestForResponseBytes(request)
   266  	response = string(bytes)
   267  	return
   268  }
   269  
   270  func (gateway Gateway) PerformRequestForJSONResponse(request *Request, response interface{}) (headers http.Header, apiErr error) {
   271  	bytes, headers, rawResponse, apiErr := gateway.performRequestForResponseBytes(request)
   272  	if apiErr != nil {
   273  		return
   274  	}
   275  
   276  	if rawResponse.StatusCode > 203 || strings.TrimSpace(string(bytes)) == "" {
   277  		return
   278  	}
   279  
   280  	err := json.Unmarshal(bytes, &response)
   281  	if err != nil {
   282  		apiErr = errors.NewWithError(T("Invalid JSON response from server"), err)
   283  	}
   284  	return
   285  }
   286  
   287  func (gateway Gateway) PerformPollingRequestForJSONResponse(endpoint string, request *Request, response interface{}, timeout time.Duration) (headers http.Header, apiErr error) {
   288  	query := request.HttpReq.URL.Query()
   289  	query.Add("async", "true")
   290  	request.HttpReq.URL.RawQuery = query.Encode()
   291  
   292  	bytes, headers, rawResponse, apiErr := gateway.performRequestForResponseBytes(request)
   293  	if apiErr != nil {
   294  		return
   295  	}
   296  	defer rawResponse.Body.Close()
   297  
   298  	if rawResponse.StatusCode > 203 || strings.TrimSpace(string(bytes)) == "" {
   299  		return
   300  	}
   301  
   302  	err := json.Unmarshal(bytes, &response)
   303  	if err != nil {
   304  		apiErr = errors.NewWithError(T("Invalid JSON response from server"), err)
   305  		return
   306  	}
   307  
   308  	asyncResource := &AsyncResource{}
   309  	err = json.Unmarshal(bytes, &asyncResource)
   310  	if err != nil {
   311  		apiErr = errors.NewWithError(T("Invalid async response from server"), err)
   312  		return
   313  	}
   314  
   315  	jobUrl := asyncResource.Metadata.URL
   316  	if jobUrl == "" {
   317  		return
   318  	}
   319  
   320  	if !strings.Contains(jobUrl, "/jobs/") {
   321  		return
   322  	}
   323  
   324  	apiErr = gateway.waitForJob(endpoint+jobUrl, request.HttpReq.Header.Get("Authorization"), timeout)
   325  
   326  	return
   327  }
   328  
   329  func (gateway Gateway) Warnings() []string {
   330  	return *gateway.warnings
   331  }
   332  
   333  func (gateway Gateway) waitForJob(jobUrl, accessToken string, timeout time.Duration) (err error) {
   334  	startTime := gateway.Clock()
   335  	for true {
   336  		if gateway.Clock().Sub(startTime) > timeout && timeout != 0 {
   337  			return errors.NewAsyncTimeoutError(jobUrl)
   338  		}
   339  		var request *Request
   340  		request, err = gateway.NewRequest("GET", jobUrl, accessToken, nil)
   341  		response := &JobResource{}
   342  		_, err = gateway.PerformRequestForJSONResponse(request, response)
   343  		if err != nil {
   344  			return
   345  		}
   346  
   347  		switch response.Entity.Status {
   348  		case JOB_FINISHED:
   349  			return
   350  		case JOB_FAILED:
   351  			err = errors.New(response.Entity.ErrorDetails.Description)
   352  			return
   353  		}
   354  
   355  		accessToken = request.HttpReq.Header.Get("Authorization")
   356  
   357  		time.Sleep(gateway.PollingThrottle)
   358  	}
   359  	return
   360  }
   361  
   362  func (gateway Gateway) doRequestHandlingAuth(request *Request) (rawResponse *http.Response, err error) {
   363  	httpReq := request.HttpReq
   364  
   365  	if request.SeekableBody != nil {
   366  		httpReq.Body = ioutil.NopCloser(request.SeekableBody)
   367  	}
   368  
   369  	// perform request
   370  	rawResponse, err = gateway.doRequestAndHandlerError(request)
   371  	if err == nil || gateway.authenticator == nil {
   372  		return
   373  	}
   374  
   375  	switch err.(type) {
   376  	case *errors.InvalidTokenError:
   377  		// refresh the auth token
   378  		var newToken string
   379  		newToken, err = gateway.authenticator.RefreshAuthToken()
   380  		if err != nil {
   381  			return
   382  		}
   383  
   384  		// reset the auth token and request body
   385  		httpReq.Header.Set("Authorization", newToken)
   386  		if request.SeekableBody != nil {
   387  			request.SeekableBody.Seek(0, 0)
   388  			httpReq.Body = ioutil.NopCloser(request.SeekableBody)
   389  		}
   390  
   391  		// make the request again
   392  		rawResponse, err = gateway.doRequestAndHandlerError(request)
   393  	}
   394  
   395  	return
   396  }
   397  
   398  func (gateway Gateway) doRequestAndHandlerError(request *Request) (rawResponse *http.Response, err error) {
   399  	rawResponse, err = gateway.doRequest(request.HttpReq)
   400  	if err != nil {
   401  		err = WrapNetworkErrors(request.HttpReq.URL.Host, err)
   402  		return
   403  	}
   404  
   405  	if rawResponse.StatusCode > 299 {
   406  		jsonBytes, _ := ioutil.ReadAll(rawResponse.Body)
   407  		rawResponse.Body.Close()
   408  		rawResponse.Body = ioutil.NopCloser(bytes.NewBuffer(jsonBytes))
   409  		err = gateway.errHandler(rawResponse.StatusCode, jsonBytes)
   410  	}
   411  
   412  	return
   413  }
   414  
   415  func (gateway Gateway) doRequest(request *http.Request) (response *http.Response, err error) {
   416  	if gateway.transport == nil {
   417  		makeHttpTransport(&gateway)
   418  	}
   419  
   420  	httpClient := NewHttpClient(gateway.transport)
   421  
   422  	dumpRequest(request)
   423  
   424  	for i := 0; i < 3; i++ {
   425  		response, err = httpClient.Do(request)
   426  		if response == nil && err != nil {
   427  			continue
   428  		} else {
   429  			break
   430  		}
   431  	}
   432  
   433  	if err != nil {
   434  		return
   435  	}
   436  
   437  	dumpResponse(response)
   438  
   439  	header := http.CanonicalHeaderKey("X-Cf-Warnings")
   440  	raw_warnings := response.Header[header]
   441  	for _, raw_warning := range raw_warnings {
   442  		warning, _ := url.QueryUnescape(raw_warning)
   443  		*gateway.warnings = append(*gateway.warnings, warning)
   444  	}
   445  
   446  	return
   447  }
   448  
   449  func makeHttpTransport(gateway *Gateway) {
   450  	gateway.transport = &http.Transport{
   451  		Dial:            (&net.Dialer{Timeout: 5 * time.Second}).Dial,
   452  		TLSClientConfig: NewTLSConfig(gateway.trustedCerts, gateway.config.IsSSLDisabled()),
   453  		Proxy:           http.ProxyFromEnvironment,
   454  	}
   455  }
   456  
   457  func (gateway *Gateway) SetTrustedCerts(certificates []tls.Certificate) {
   458  	gateway.trustedCerts = certificates
   459  	makeHttpTransport(gateway)
   460  }