github.com/swisscom/cloudfoundry-cli@v7.1.0+incompatible/cf/net/gateway.go (about)

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