github.com/jasonkeene/cli@v6.14.1-0.20160816203908-ca5715166dfb+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  	"github.com/cloudfoundry/cli/cf"
    20  	"github.com/cloudfoundry/cli/cf/configuration/coreconfig"
    21  	"github.com/cloudfoundry/cli/cf/errors"
    22  	. "github.com/cloudfoundry/cli/cf/i18n"
    23  	"github.com/cloudfoundry/cli/cf/terminal"
    24  	"github.com/cloudfoundry/cli/cf/trace"
    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("Connection", "close")
   202  	request.Header.Set("content-type", "application/json")
   203  	request.Header.Set("User-Agent", "go-cli "+cf.Version+" / "+runtime.GOOS)
   204  
   205  	return &Request{HTTPReq: request, SeekableBody: body}
   206  }
   207  
   208  func (gateway Gateway) NewRequestForFile(method, fullURL, accessToken string, body *os.File) (*Request, error) {
   209  	progressReader := NewProgressReader(body, gateway.ui, 5*time.Second)
   210  	_, _ = progressReader.Seek(0, 0)
   211  
   212  	fileStats, err := body.Stat()
   213  	if err != nil {
   214  		return nil, fmt.Errorf("%s: %s", T("Error getting file info"), err.Error())
   215  	}
   216  
   217  	request, err := http.NewRequest(method, fullURL, progressReader)
   218  	if err != nil {
   219  		return nil, fmt.Errorf("%s: %s", T("Error building request"), err.Error())
   220  	}
   221  
   222  	fileSize := fileStats.Size()
   223  	progressReader.SetTotalSize(fileSize)
   224  	request.ContentLength = fileSize
   225  
   226  	if err != nil {
   227  		return nil, fmt.Errorf("%s: %s", T("Error building request"), err.Error())
   228  	}
   229  
   230  	return gateway.newRequest(request, accessToken, progressReader), nil
   231  }
   232  
   233  func (gateway Gateway) NewRequest(method, path, accessToken string, body io.ReadSeeker) (*Request, error) {
   234  	request, err := http.NewRequest(method, path, body)
   235  	if err != nil {
   236  		return nil, fmt.Errorf("%s: %s", T("Error building request"), err.Error())
   237  	}
   238  	return gateway.newRequest(request, accessToken, body), nil
   239  }
   240  
   241  func (gateway Gateway) PerformRequest(request *Request) (*http.Response, error) {
   242  	return gateway.doRequestHandlingAuth(request)
   243  }
   244  
   245  func (gateway Gateway) performRequestForResponseBytes(request *Request) ([]byte, http.Header, *http.Response, error) {
   246  	rawResponse, err := gateway.doRequestHandlingAuth(request)
   247  	if err != nil {
   248  		return nil, nil, rawResponse, err
   249  	}
   250  	defer rawResponse.Body.Close()
   251  
   252  	bytes, err := ioutil.ReadAll(rawResponse.Body)
   253  	if err != nil {
   254  		return bytes, nil, rawResponse, fmt.Errorf("%s: %s", T("Error reading response"), err.Error())
   255  	}
   256  
   257  	return bytes, rawResponse.Header, rawResponse, nil
   258  }
   259  
   260  func (gateway Gateway) PerformRequestForTextResponse(request *Request) (string, http.Header, error) {
   261  	bytes, headers, _, err := gateway.performRequestForResponseBytes(request)
   262  	return string(bytes), headers, err
   263  }
   264  
   265  func (gateway Gateway) PerformRequestForJSONResponse(request *Request, response interface{}) (http.Header, error) {
   266  	bytes, headers, rawResponse, err := gateway.performRequestForResponseBytes(request)
   267  	if err != nil {
   268  		if rawResponse != nil && rawResponse.Body != nil {
   269  			b, _ := ioutil.ReadAll(rawResponse.Body)
   270  			_ = json.Unmarshal(b, &response)
   271  		}
   272  		return headers, err
   273  	}
   274  
   275  	if rawResponse.StatusCode > 203 || strings.TrimSpace(string(bytes)) == "" {
   276  		return headers, nil
   277  	}
   278  
   279  	err = json.Unmarshal(bytes, &response)
   280  	if err != nil {
   281  		return headers, fmt.Errorf("%s: %s", T("Invalid JSON response from server"), err.Error())
   282  	}
   283  
   284  	return headers, nil
   285  }
   286  
   287  func (gateway Gateway) PerformPollingRequestForJSONResponse(endpoint string, request *Request, response interface{}, timeout time.Duration) (http.Header, error) {
   288  	query := request.HTTPReq.URL.Query()
   289  	query.Add("async", "true")
   290  	request.HTTPReq.URL.RawQuery = query.Encode()
   291  
   292  	bytes, headers, rawResponse, err := gateway.performRequestForResponseBytes(request)
   293  	if err != nil {
   294  		return headers, err
   295  	}
   296  	defer rawResponse.Body.Close()
   297  
   298  	if rawResponse.StatusCode > 203 || strings.TrimSpace(string(bytes)) == "" {
   299  		return headers, nil
   300  	}
   301  
   302  	err = json.Unmarshal(bytes, &response)
   303  	if err != nil {
   304  		return headers, fmt.Errorf("%s: %s", T("Invalid JSON response from server"), err.Error())
   305  	}
   306  
   307  	asyncResource := &AsyncResource{}
   308  	err = json.Unmarshal(bytes, &asyncResource)
   309  	if err != nil {
   310  		return headers, fmt.Errorf("%s: %s", T("Invalid async response from server"), err.Error())
   311  	}
   312  
   313  	jobURL := asyncResource.Metadata.URL
   314  	if jobURL == "" {
   315  		return headers, nil
   316  	}
   317  
   318  	if !strings.Contains(jobURL, "/jobs/") {
   319  		return headers, nil
   320  	}
   321  
   322  	err = gateway.waitForJob(endpoint+jobURL, request.HTTPReq.Header.Get("Authorization"), timeout)
   323  
   324  	return headers, err
   325  }
   326  
   327  func (gateway Gateway) Warnings() []string {
   328  	return *gateway.warnings
   329  }
   330  
   331  func (gateway Gateway) waitForJob(jobURL, accessToken string, timeout time.Duration) error {
   332  	startTime := gateway.Clock()
   333  	for true {
   334  		if gateway.Clock().Sub(startTime) > timeout && timeout != 0 {
   335  			return errors.NewAsyncTimeoutError(jobURL)
   336  		}
   337  		var request *Request
   338  		request, err := gateway.NewRequest("GET", jobURL, accessToken, nil)
   339  		response := &JobResource{}
   340  		_, err = gateway.PerformRequestForJSONResponse(request, response)
   341  		if err != nil {
   342  			return err
   343  		}
   344  
   345  		switch response.Entity.Status {
   346  		case JobFinished:
   347  			return nil
   348  		case JobFailed:
   349  			return errors.New(response.Entity.ErrorDetails.Description)
   350  		}
   351  
   352  		accessToken = request.HTTPReq.Header.Get("Authorization")
   353  
   354  		time.Sleep(gateway.PollingThrottle)
   355  	}
   356  	return nil
   357  }
   358  
   359  func (gateway Gateway) doRequestHandlingAuth(request *Request) (*http.Response, error) {
   360  	httpReq := request.HTTPReq
   361  
   362  	if request.SeekableBody != nil {
   363  		httpReq.Body = ioutil.NopCloser(request.SeekableBody)
   364  	}
   365  
   366  	// perform request
   367  	rawResponse, err := gateway.doRequestAndHandlerError(request)
   368  	if err == nil || gateway.authenticator == nil {
   369  		return rawResponse, err
   370  	}
   371  
   372  	switch err.(type) {
   373  	case *errors.InvalidTokenError:
   374  		// refresh the auth token
   375  		var newToken string
   376  		newToken, err = gateway.authenticator.RefreshAuthToken()
   377  		if err != nil {
   378  			return rawResponse, err
   379  		}
   380  
   381  		// reset the auth token and request body
   382  		httpReq.Header.Set("Authorization", newToken)
   383  		if request.SeekableBody != nil {
   384  			_, _ = request.SeekableBody.Seek(0, 0)
   385  			httpReq.Body = ioutil.NopCloser(request.SeekableBody)
   386  		}
   387  
   388  		// make the request again
   389  		rawResponse, err = gateway.doRequestAndHandlerError(request)
   390  	}
   391  
   392  	return rawResponse, err
   393  }
   394  
   395  func (gateway Gateway) doRequestAndHandlerError(request *Request) (*http.Response, error) {
   396  	rawResponse, err := gateway.doRequest(request.HTTPReq)
   397  	if err != nil {
   398  		return rawResponse, WrapNetworkErrors(request.HTTPReq.URL.Host, err)
   399  	}
   400  
   401  	if rawResponse.StatusCode > 299 {
   402  		defer rawResponse.Body.Close()
   403  		jsonBytes, _ := ioutil.ReadAll(rawResponse.Body)
   404  		rawResponse.Body = ioutil.NopCloser(bytes.NewBuffer(jsonBytes))
   405  		err = gateway.errHandler(rawResponse.StatusCode, jsonBytes)
   406  	}
   407  
   408  	return rawResponse, err
   409  }
   410  
   411  func (gateway Gateway) doRequest(request *http.Request) (*http.Response, error) {
   412  	var response *http.Response
   413  	var err error
   414  
   415  	if gateway.transport == nil {
   416  		makeHTTPTransport(&gateway)
   417  	}
   418  
   419  	httpClient := NewHTTPClient(gateway.transport, NewRequestDumper(gateway.logger))
   420  
   421  	httpClient.DumpRequest(request)
   422  
   423  	for i := 0; i < 3; i++ {
   424  		response, err = httpClient.Do(request)
   425  		if response == nil && err != nil {
   426  			continue
   427  		} else {
   428  			break
   429  		}
   430  	}
   431  
   432  	if err != nil {
   433  		return response, err
   434  	}
   435  
   436  	httpClient.DumpResponse(response)
   437  
   438  	header := http.CanonicalHeaderKey("X-Cf-Warnings")
   439  	rawWarnings := response.Header[header]
   440  	for _, rawWarning := range rawWarnings {
   441  		warning, _ := url.QueryUnescape(rawWarning)
   442  		*gateway.warnings = append(*gateway.warnings, warning)
   443  	}
   444  
   445  	return response, err
   446  }
   447  
   448  func makeHTTPTransport(gateway *Gateway) {
   449  	gateway.transport = &http.Transport{
   450  		Dial:            (&net.Dialer{Timeout: gateway.DialTimeout}).Dial,
   451  		TLSClientConfig: NewTLSConfig(gateway.trustedCerts, gateway.config.IsSSLDisabled()),
   452  		Proxy:           http.ProxyFromEnvironment,
   453  	}
   454  }
   455  
   456  func dialTimeout(envDialTimeout string) time.Duration {
   457  	dialTimeout := DefaultDialTimeout
   458  	if timeout, err := strconv.Atoi(envDialTimeout); err == nil {
   459  		dialTimeout = time.Duration(timeout) * time.Second
   460  	}
   461  	return dialTimeout
   462  }
   463  
   464  func (gateway *Gateway) SetTrustedCerts(certificates []tls.Certificate) {
   465  	gateway.trustedCerts = certificates
   466  	makeHTTPTransport(gateway)
   467  }