github.com/vnpaycloud-console/gophercloud/v2@v2.0.5/provider_client.go (about)

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