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