github.com/gophercloud/gophercloud@v1.11.0/provider_client.go (about)

     1  package gophercloud
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"encoding/json"
     7  	"errors"
     8  	"io"
     9  	"io/ioutil"
    10  	"net/http"
    11  	"strings"
    12  	"sync"
    13  )
    14  
    15  // DefaultUserAgent is the default User-Agent string set in the request header.
    16  const (
    17  	DefaultUserAgent         = "gophercloud/v1.11.0"
    18  	DefaultMaxBackoffRetries = 60
    19  )
    20  
    21  // UserAgent represents a User-Agent header.
    22  type UserAgent struct {
    23  	// prepend is the slice of User-Agent strings to prepend to DefaultUserAgent.
    24  	// All the strings to prepend are accumulated and prepended in the Join method.
    25  	prepend []string
    26  }
    27  
    28  type RetryBackoffFunc func(context.Context, *ErrUnexpectedResponseCode, error, uint) error
    29  
    30  // RetryFunc is a catch-all function for retrying failed API requests.
    31  // If it returns nil, the request will be retried.  If it returns an error,
    32  // the request method will exit with that error.  failCount is the number of
    33  // times the request has failed (starting at 1).
    34  type RetryFunc func(context context.Context, method, url string, options *RequestOpts, err error, failCount uint) error
    35  
    36  // Prepend prepends a user-defined string to the default User-Agent string. Users
    37  // may pass in one or more strings to prepend.
    38  func (ua *UserAgent) Prepend(s ...string) {
    39  	ua.prepend = append(s, ua.prepend...)
    40  }
    41  
    42  // Join concatenates all the user-defined User-Agend strings with the default
    43  // Gophercloud User-Agent string.
    44  func (ua *UserAgent) Join() string {
    45  	uaSlice := append(ua.prepend, DefaultUserAgent)
    46  	return strings.Join(uaSlice, " ")
    47  }
    48  
    49  // ProviderClient stores details that are required to interact with any
    50  // services within a specific provider's API.
    51  //
    52  // Generally, you acquire a ProviderClient by calling the NewClient method in
    53  // the appropriate provider's child package, providing whatever authentication
    54  // credentials are required.
    55  type ProviderClient struct {
    56  	// IdentityBase is the base URL used for a particular provider's identity
    57  	// service - it will be used when issuing authenticatation requests. It
    58  	// should point to the root resource of the identity service, not a specific
    59  	// identity version.
    60  	IdentityBase string
    61  
    62  	// IdentityEndpoint is the identity endpoint. This may be a specific version
    63  	// of the identity service. If this is the case, this endpoint is used rather
    64  	// than querying versions first.
    65  	IdentityEndpoint string
    66  
    67  	// TokenID is the ID of the most recently issued valid token.
    68  	// NOTE: Aside from within a custom ReauthFunc, this field shouldn't be set by an application.
    69  	// To safely read or write this value, call `Token` or `SetToken`, respectively
    70  	TokenID string
    71  
    72  	// EndpointLocator describes how this provider discovers the endpoints for
    73  	// its constituent services.
    74  	EndpointLocator EndpointLocator
    75  
    76  	// HTTPClient allows users to interject arbitrary http, https, or other transit behaviors.
    77  	HTTPClient http.Client
    78  
    79  	// UserAgent represents the User-Agent header in the HTTP request.
    80  	UserAgent UserAgent
    81  
    82  	// ReauthFunc is the function used to re-authenticate the user if the request
    83  	// fails with a 401 HTTP response code. This a needed because there may be multiple
    84  	// authentication functions for different Identity service versions.
    85  	ReauthFunc func() error
    86  
    87  	// Throwaway determines whether if this client is a throw-away client. It's a copy of user's provider client
    88  	// with the token and reauth func zeroed. Such client can be used to perform reauthorization.
    89  	Throwaway bool
    90  
    91  	// Context is the context passed to the HTTP request.
    92  	Context context.Context
    93  
    94  	// Retry backoff func is called when rate limited.
    95  	RetryBackoffFunc RetryBackoffFunc
    96  
    97  	// MaxBackoffRetries set the maximum number of backoffs. When not set, defaults to DefaultMaxBackoffRetries
    98  	MaxBackoffRetries uint
    99  
   100  	// A general failed request handler method - this is always called in the end if a request failed. Leave as nil
   101  	// to abort when an error is encountered.
   102  	RetryFunc RetryFunc
   103  
   104  	// mut is a mutex for the client. It protects read and write access to client attributes such as getting
   105  	// and setting the TokenID.
   106  	mut *sync.RWMutex
   107  
   108  	// reauthmut is a mutex for reauthentication it attempts to ensure that only one reauthentication
   109  	// attempt happens at one time.
   110  	reauthmut *reauthlock
   111  
   112  	authResult AuthResult
   113  }
   114  
   115  // reauthlock represents a set of attributes used to help in the reauthentication process.
   116  type reauthlock struct {
   117  	sync.RWMutex
   118  	ongoing *reauthFuture
   119  }
   120  
   121  // reauthFuture represents future result of the reauthentication process.
   122  // while done channel is not closed, reauthentication is in progress.
   123  // when done channel is closed, err contains the result of reauthentication.
   124  type reauthFuture struct {
   125  	done chan struct{}
   126  	err  error
   127  }
   128  
   129  func newReauthFuture() *reauthFuture {
   130  	return &reauthFuture{
   131  		make(chan struct{}),
   132  		nil,
   133  	}
   134  }
   135  
   136  func (f *reauthFuture) Set(err error) {
   137  	f.err = err
   138  	close(f.done)
   139  }
   140  
   141  func (f *reauthFuture) Get() error {
   142  	<-f.done
   143  	return f.err
   144  }
   145  
   146  // AuthenticatedHeaders returns a map of HTTP headers that are common for all
   147  // authenticated service requests. Blocks if Reauthenticate is in progress.
   148  func (client *ProviderClient) AuthenticatedHeaders() (m map[string]string) {
   149  	if client.IsThrowaway() {
   150  		return
   151  	}
   152  	if client.reauthmut != nil {
   153  		// If a Reauthenticate is in progress, wait for it to complete.
   154  		client.reauthmut.Lock()
   155  		ongoing := client.reauthmut.ongoing
   156  		client.reauthmut.Unlock()
   157  		if ongoing != nil {
   158  			_ = ongoing.Get()
   159  		}
   160  	}
   161  	t := client.Token()
   162  	if t == "" {
   163  		return
   164  	}
   165  	return map[string]string{"X-Auth-Token": t}
   166  }
   167  
   168  // UseTokenLock creates a mutex that is used to allow safe concurrent access to the auth token.
   169  // If the application's ProviderClient is not used concurrently, this doesn't need to be called.
   170  func (client *ProviderClient) UseTokenLock() {
   171  	client.mut = new(sync.RWMutex)
   172  	client.reauthmut = new(reauthlock)
   173  }
   174  
   175  // GetAuthResult returns the result from the request that was used to obtain a
   176  // provider client's Keystone token.
   177  //
   178  // The result is nil when authentication has not yet taken place, when the token
   179  // was set manually with SetToken(), or when a ReauthFunc was used that does not
   180  // record the AuthResult.
   181  func (client *ProviderClient) GetAuthResult() AuthResult {
   182  	if client.mut != nil {
   183  		client.mut.RLock()
   184  		defer client.mut.RUnlock()
   185  	}
   186  	return client.authResult
   187  }
   188  
   189  // Token safely reads the value of the auth token from the ProviderClient. Applications should
   190  // call this method to access the token instead of the TokenID field
   191  func (client *ProviderClient) Token() string {
   192  	if client.mut != nil {
   193  		client.mut.RLock()
   194  		defer client.mut.RUnlock()
   195  	}
   196  	return client.TokenID
   197  }
   198  
   199  // SetToken safely sets the value of the auth token in the ProviderClient. Applications may
   200  // use this method in a custom ReauthFunc.
   201  //
   202  // WARNING: This function is deprecated. Use SetTokenAndAuthResult() instead.
   203  func (client *ProviderClient) SetToken(t string) {
   204  	if client.mut != nil {
   205  		client.mut.Lock()
   206  		defer client.mut.Unlock()
   207  	}
   208  	client.TokenID = t
   209  	client.authResult = nil
   210  }
   211  
   212  // SetTokenAndAuthResult safely sets the value of the auth token in the
   213  // ProviderClient and also records the AuthResult that was returned from the
   214  // token creation request. Applications may call this in a custom ReauthFunc.
   215  func (client *ProviderClient) SetTokenAndAuthResult(r AuthResult) error {
   216  	tokenID := ""
   217  	var err error
   218  	if r != nil {
   219  		tokenID, err = r.ExtractTokenID()
   220  		if err != nil {
   221  			return err
   222  		}
   223  	}
   224  
   225  	if client.mut != nil {
   226  		client.mut.Lock()
   227  		defer client.mut.Unlock()
   228  	}
   229  	client.TokenID = tokenID
   230  	client.authResult = r
   231  	return nil
   232  }
   233  
   234  // CopyTokenFrom safely copies the token from another ProviderClient into the
   235  // this one.
   236  func (client *ProviderClient) CopyTokenFrom(other *ProviderClient) {
   237  	if client.mut != nil {
   238  		client.mut.Lock()
   239  		defer client.mut.Unlock()
   240  	}
   241  	if other.mut != nil && other.mut != client.mut {
   242  		other.mut.RLock()
   243  		defer other.mut.RUnlock()
   244  	}
   245  	client.TokenID = other.TokenID
   246  	client.authResult = other.authResult
   247  }
   248  
   249  // IsThrowaway safely reads the value of the client Throwaway field.
   250  func (client *ProviderClient) IsThrowaway() bool {
   251  	if client.reauthmut != nil {
   252  		client.reauthmut.RLock()
   253  		defer client.reauthmut.RUnlock()
   254  	}
   255  	return client.Throwaway
   256  }
   257  
   258  // SetThrowaway safely sets the value of the client Throwaway field.
   259  func (client *ProviderClient) SetThrowaway(v bool) {
   260  	if client.reauthmut != nil {
   261  		client.reauthmut.Lock()
   262  		defer client.reauthmut.Unlock()
   263  	}
   264  	client.Throwaway = v
   265  }
   266  
   267  // Reauthenticate calls client.ReauthFunc in a thread-safe way. If this is
   268  // called because of a 401 response, the caller may pass the previous token. In
   269  // this case, the reauthentication can be skipped if another thread has already
   270  // reauthenticated in the meantime. If no previous token is known, an empty
   271  // string should be passed instead to force unconditional reauthentication.
   272  func (client *ProviderClient) Reauthenticate(previousToken string) error {
   273  	if client.ReauthFunc == nil {
   274  		return nil
   275  	}
   276  
   277  	if client.reauthmut == nil {
   278  		return client.ReauthFunc()
   279  	}
   280  
   281  	future := newReauthFuture()
   282  
   283  	// Check if a Reauthenticate is in progress, or start one if not.
   284  	client.reauthmut.Lock()
   285  	ongoing := client.reauthmut.ongoing
   286  	if ongoing == nil {
   287  		client.reauthmut.ongoing = future
   288  	}
   289  	client.reauthmut.Unlock()
   290  
   291  	// If Reauthenticate is running elsewhere, wait for its result.
   292  	if ongoing != nil {
   293  		return ongoing.Get()
   294  	}
   295  
   296  	// Perform the actual reauthentication.
   297  	var err error
   298  	if previousToken == "" || client.TokenID == previousToken {
   299  		err = client.ReauthFunc()
   300  	} else {
   301  		err = nil
   302  	}
   303  
   304  	// Mark Reauthenticate as finished.
   305  	client.reauthmut.Lock()
   306  	client.reauthmut.ongoing.Set(err)
   307  	client.reauthmut.ongoing = nil
   308  	client.reauthmut.Unlock()
   309  
   310  	return err
   311  }
   312  
   313  // RequestOpts customizes the behavior of the provider.Request() method.
   314  type RequestOpts struct {
   315  	// JSONBody, if provided, will be encoded as JSON and used as the body of the HTTP request. The
   316  	// content type of the request will default to "application/json" unless overridden by MoreHeaders.
   317  	// It's an error to specify both a JSONBody and a RawBody.
   318  	JSONBody interface{}
   319  	// RawBody contains an io.Reader that will be consumed by the request directly. No content-type
   320  	// will be set unless one is provided explicitly by MoreHeaders.
   321  	RawBody io.Reader
   322  	// JSONResponse, if provided, will be populated with the contents of the response body parsed as
   323  	// JSON.
   324  	JSONResponse interface{}
   325  	// OkCodes contains a list of numeric HTTP status codes that should be interpreted as success. If
   326  	// the response has a different code, an error will be returned.
   327  	OkCodes []int
   328  	// MoreHeaders specifies additional HTTP headers to be provided on the request.
   329  	// MoreHeaders will be overridden by OmitHeaders
   330  	MoreHeaders map[string]string
   331  	// OmitHeaders specifies the HTTP headers which should be omitted.
   332  	// OmitHeaders will override MoreHeaders
   333  	OmitHeaders []string
   334  	// ErrorContext specifies the resource error type to return if an error is encountered.
   335  	// This lets resources override default error messages based on the response status code.
   336  	ErrorContext error
   337  	// KeepResponseBody specifies whether to keep the HTTP response body. Usually used, when the HTTP
   338  	// response body is considered for further use. Valid when JSONResponse is nil.
   339  	KeepResponseBody bool
   340  }
   341  
   342  // requestState contains temporary state for a single ProviderClient.Request() call.
   343  type requestState struct {
   344  	// This flag indicates if we have reauthenticated during this request because of a 401 response.
   345  	// It ensures that we don't reauthenticate multiple times for a single request. If we
   346  	// reauthenticate, but keep getting 401 responses with the fresh token, reauthenticating some more
   347  	// will just get us into an infinite loop.
   348  	hasReauthenticated bool
   349  	// Retry-After backoff counter, increments during each backoff call
   350  	retries uint
   351  }
   352  
   353  var applicationJSON = "application/json"
   354  
   355  // Request performs an HTTP request using the ProviderClient's current HTTPClient. An authentication
   356  // header will automatically be provided.
   357  func (client *ProviderClient) Request(method, url string, options *RequestOpts) (*http.Response, error) {
   358  	return client.doRequest(method, url, options, &requestState{
   359  		hasReauthenticated: false,
   360  	})
   361  }
   362  
   363  func (client *ProviderClient) doRequest(method, url string, options *RequestOpts, state *requestState) (*http.Response, error) {
   364  	var body io.Reader
   365  	var contentType *string
   366  
   367  	// Derive the content body by either encoding an arbitrary object as JSON, or by taking a provided
   368  	// io.ReadSeeker as-is. Default the content-type to application/json.
   369  	if options.JSONBody != nil {
   370  		if options.RawBody != nil {
   371  			return nil, errors.New("please provide only one of JSONBody or RawBody to gophercloud.Request()")
   372  		}
   373  
   374  		rendered, err := json.Marshal(options.JSONBody)
   375  		if err != nil {
   376  			return nil, err
   377  		}
   378  
   379  		body = bytes.NewReader(rendered)
   380  		contentType = &applicationJSON
   381  	}
   382  
   383  	// Return an error, when "KeepResponseBody" is true and "JSONResponse" is not nil
   384  	if options.KeepResponseBody && options.JSONResponse != nil {
   385  		return nil, errors.New("cannot use KeepResponseBody when JSONResponse is not nil")
   386  	}
   387  
   388  	if options.RawBody != nil {
   389  		body = options.RawBody
   390  	}
   391  
   392  	// Construct the http.Request.
   393  	req, err := http.NewRequest(method, url, body)
   394  	if err != nil {
   395  		return nil, err
   396  	}
   397  	if client.Context != nil {
   398  		req = req.WithContext(client.Context)
   399  	}
   400  
   401  	// Populate the request headers.
   402  	// Apply options.MoreHeaders and options.OmitHeaders, to give the caller the chance to
   403  	// modify or omit any header.
   404  	if contentType != nil {
   405  		req.Header.Set("Content-Type", *contentType)
   406  	}
   407  	req.Header.Set("Accept", applicationJSON)
   408  
   409  	// Set the User-Agent header
   410  	req.Header.Set("User-Agent", client.UserAgent.Join())
   411  
   412  	if options.MoreHeaders != nil {
   413  		for k, v := range options.MoreHeaders {
   414  			req.Header.Set(k, v)
   415  		}
   416  	}
   417  
   418  	for _, v := range options.OmitHeaders {
   419  		req.Header.Del(v)
   420  	}
   421  
   422  	// get latest token from client
   423  	for k, v := range client.AuthenticatedHeaders() {
   424  		req.Header.Set(k, v)
   425  	}
   426  
   427  	prereqtok := req.Header.Get("X-Auth-Token")
   428  
   429  	// Issue the request.
   430  	resp, err := client.HTTPClient.Do(req)
   431  	if err != nil {
   432  		if client.RetryFunc != nil {
   433  			var e error
   434  			state.retries = state.retries + 1
   435  			e = client.RetryFunc(client.Context, method, url, options, err, state.retries)
   436  			if e != nil {
   437  				return nil, e
   438  			}
   439  
   440  			return client.doRequest(method, url, options, state)
   441  		}
   442  		return nil, err
   443  	}
   444  
   445  	// Allow default OkCodes if none explicitly set
   446  	okc := options.OkCodes
   447  	if okc == nil {
   448  		okc = defaultOkCodes(method)
   449  	}
   450  
   451  	// Validate the HTTP response status.
   452  	var ok bool
   453  	for _, code := range okc {
   454  		if resp.StatusCode == code {
   455  			ok = true
   456  			break
   457  		}
   458  	}
   459  
   460  	if !ok {
   461  		body, _ := ioutil.ReadAll(resp.Body)
   462  		resp.Body.Close()
   463  		respErr := ErrUnexpectedResponseCode{
   464  			URL:            url,
   465  			Method:         method,
   466  			Expected:       okc,
   467  			Actual:         resp.StatusCode,
   468  			Body:           body,
   469  			ResponseHeader: resp.Header,
   470  		}
   471  
   472  		errType := options.ErrorContext
   473  		switch resp.StatusCode {
   474  		case http.StatusBadRequest:
   475  			err = ErrDefault400{respErr}
   476  			if error400er, ok := errType.(Err400er); ok {
   477  				err = error400er.Error400(respErr)
   478  			}
   479  		case http.StatusUnauthorized:
   480  			if client.ReauthFunc != nil && !state.hasReauthenticated {
   481  				err = client.Reauthenticate(prereqtok)
   482  				if err != nil {
   483  					e := &ErrUnableToReauthenticate{}
   484  					e.ErrOriginal = respErr
   485  					e.ErrReauth = err
   486  					return nil, e
   487  				}
   488  				if options.RawBody != nil {
   489  					if seeker, ok := options.RawBody.(io.Seeker); ok {
   490  						seeker.Seek(0, 0)
   491  					}
   492  				}
   493  				state.hasReauthenticated = true
   494  				resp, err = client.doRequest(method, url, options, state)
   495  				if err != nil {
   496  					switch err.(type) {
   497  					case *ErrUnexpectedResponseCode:
   498  						e := &ErrErrorAfterReauthentication{}
   499  						e.ErrOriginal = err.(*ErrUnexpectedResponseCode)
   500  						return nil, e
   501  					default:
   502  						e := &ErrErrorAfterReauthentication{}
   503  						e.ErrOriginal = err
   504  						return nil, e
   505  					}
   506  				}
   507  				return resp, nil
   508  			}
   509  			err = ErrDefault401{respErr}
   510  			if error401er, ok := errType.(Err401er); ok {
   511  				err = error401er.Error401(respErr)
   512  			}
   513  		case http.StatusForbidden:
   514  			err = ErrDefault403{respErr}
   515  			if error403er, ok := errType.(Err403er); ok {
   516  				err = error403er.Error403(respErr)
   517  			}
   518  		case http.StatusNotFound:
   519  			err = ErrDefault404{respErr}
   520  			if error404er, ok := errType.(Err404er); ok {
   521  				err = error404er.Error404(respErr)
   522  			}
   523  		case http.StatusMethodNotAllowed:
   524  			err = ErrDefault405{respErr}
   525  			if error405er, ok := errType.(Err405er); ok {
   526  				err = error405er.Error405(respErr)
   527  			}
   528  		case http.StatusRequestTimeout:
   529  			err = ErrDefault408{respErr}
   530  			if error408er, ok := errType.(Err408er); ok {
   531  				err = error408er.Error408(respErr)
   532  			}
   533  		case http.StatusConflict:
   534  			err = ErrDefault409{respErr}
   535  			if error409er, ok := errType.(Err409er); ok {
   536  				err = error409er.Error409(respErr)
   537  			}
   538  		case http.StatusTooManyRequests, 498:
   539  			err = ErrDefault429{respErr}
   540  			if error429er, ok := errType.(Err429er); ok {
   541  				err = error429er.Error429(respErr)
   542  			}
   543  
   544  			maxTries := client.MaxBackoffRetries
   545  			if maxTries == 0 {
   546  				maxTries = DefaultMaxBackoffRetries
   547  			}
   548  
   549  			if f := client.RetryBackoffFunc; f != nil && state.retries < maxTries {
   550  				var e error
   551  
   552  				state.retries = state.retries + 1
   553  				e = f(client.Context, &respErr, err, state.retries)
   554  
   555  				if e != nil {
   556  					return resp, e
   557  				}
   558  
   559  				return client.doRequest(method, url, options, state)
   560  			}
   561  		case http.StatusInternalServerError:
   562  			err = ErrDefault500{respErr}
   563  			if error500er, ok := errType.(Err500er); ok {
   564  				err = error500er.Error500(respErr)
   565  			}
   566  		case http.StatusBadGateway:
   567  			err = ErrDefault502{respErr}
   568  			if error502er, ok := errType.(Err502er); ok {
   569  				err = error502er.Error502(respErr)
   570  			}
   571  		case http.StatusServiceUnavailable:
   572  			err = ErrDefault503{respErr}
   573  			if error503er, ok := errType.(Err503er); ok {
   574  				err = error503er.Error503(respErr)
   575  			}
   576  		case http.StatusGatewayTimeout:
   577  			err = ErrDefault504{respErr}
   578  			if error504er, ok := errType.(Err504er); ok {
   579  				err = error504er.Error504(respErr)
   580  			}
   581  		}
   582  
   583  		if err == nil {
   584  			err = respErr
   585  		}
   586  
   587  		if err != nil && client.RetryFunc != nil {
   588  			var e error
   589  			state.retries = state.retries + 1
   590  			e = client.RetryFunc(client.Context, method, url, options, err, state.retries)
   591  			if e != nil {
   592  				return resp, e
   593  			}
   594  
   595  			return client.doRequest(method, url, options, state)
   596  		}
   597  
   598  		return resp, err
   599  	}
   600  
   601  	// Parse the response body as JSON, if requested to do so.
   602  	if options.JSONResponse != nil {
   603  		defer resp.Body.Close()
   604  		// Don't decode JSON when there is no content
   605  		if resp.StatusCode == http.StatusNoContent {
   606  			// read till EOF, otherwise the connection will be closed and cannot be reused
   607  			_, err = io.Copy(ioutil.Discard, resp.Body)
   608  			return resp, err
   609  		}
   610  		if err := json.NewDecoder(resp.Body).Decode(options.JSONResponse); err != nil {
   611  			if client.RetryFunc != nil {
   612  				var e error
   613  				state.retries = state.retries + 1
   614  				e = client.RetryFunc(client.Context, method, url, options, err, state.retries)
   615  				if e != nil {
   616  					return resp, e
   617  				}
   618  
   619  				return client.doRequest(method, url, options, state)
   620  			}
   621  			return nil, err
   622  		}
   623  	}
   624  
   625  	// Close unused body to allow the HTTP connection to be reused
   626  	if !options.KeepResponseBody && options.JSONResponse == nil {
   627  		defer resp.Body.Close()
   628  		// read till EOF, otherwise the connection will be closed and cannot be reused
   629  		if _, err := io.Copy(ioutil.Discard, resp.Body); err != nil {
   630  			return nil, err
   631  		}
   632  	}
   633  
   634  	return resp, nil
   635  }
   636  
   637  func defaultOkCodes(method string) []int {
   638  	switch method {
   639  	case "GET", "HEAD":
   640  		return []int{200}
   641  	case "POST":
   642  		return []int{201, 202}
   643  	case "PUT":
   644  		return []int{201, 202}
   645  	case "PATCH":
   646  		return []int{200, 202, 204}
   647  	case "DELETE":
   648  		return []int{202, 204}
   649  	}
   650  
   651  	return []int{}
   652  }