github.com/chnsz/golangsdk@v0.0.0-20240506093406-85a3fbfa605b/provider_client.go (about)

     1  package golangsdk
     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  	"github.com/chnsz/golangsdk/auth"
    15  )
    16  
    17  // DefaultUserAgent is the default User-Agent string set in the request header.
    18  const (
    19  	DefaultUserAgent         = "golangsdk/2.0.0"
    20  	DefaultMaxBackoffRetries = 5
    21  )
    22  
    23  // UserAgent represents a User-Agent header.
    24  type UserAgent struct {
    25  	// prepend is the slice of User-Agent strings to prepend to DefaultUserAgent.
    26  	// All the strings to prepend are accumulated and prepended in the Join method.
    27  	prepend []string
    28  }
    29  
    30  type RetryFunc func(context.Context, *ErrUnexpectedResponseCode, error, uint) error
    31  
    32  // Prepend prepends a user-defined string to the default User-Agent string. Users
    33  // may pass in one or more strings to prepend.
    34  func (ua *UserAgent) Prepend(s ...string) {
    35  	ua.prepend = append(s, ua.prepend...)
    36  }
    37  
    38  // Join concatenates all the user-defined User-Agend strings with the default
    39  // Gophercloud User-Agent string.
    40  func (ua *UserAgent) Join() string {
    41  	uaSlice := append(ua.prepend, DefaultUserAgent)
    42  	return strings.Join(uaSlice, " ")
    43  }
    44  
    45  // ProviderClient stores details that are required to interact with any
    46  // services within a specific provider's API.
    47  //
    48  // Generally, you acquire a ProviderClient by calling the NewClient method in
    49  // the appropriate provider's child package, providing whatever authentication
    50  // credentials are required.
    51  type ProviderClient struct {
    52  	// IdentityBase is the base URL used for a particular provider's identity
    53  	// service - it will be used when issuing authenticatation requests. It
    54  	// should point to the root resource of the identity service, not a specific
    55  	// identity version.
    56  	IdentityBase string
    57  
    58  	// IdentityEndpoint is the identity endpoint. This may be a specific version
    59  	// of the identity service. If this is the case, this endpoint is used rather
    60  	// than querying versions first.
    61  	IdentityEndpoint string
    62  
    63  	// TokenID is the ID of the most recently issued valid token.
    64  	// NOTE: Aside from within a custom ReauthFunc, this field shouldn't be set by an application.
    65  	// To safely read or write this value, call `Token` or `SetToken`, respectively
    66  	TokenID string
    67  
    68  	// ProjectID is the ID of project to which User is authorized.
    69  	ProjectID string
    70  
    71  	// DomainID is the ID of project to which User is authorized.
    72  	DomainID string
    73  
    74  	// EndpointLocator describes how this provider discovers the endpoints for
    75  	// its constituent services.
    76  	EndpointLocator EndpointLocator
    77  
    78  	// HTTPClient allows users to interject arbitrary http, https, or other transit behaviors.
    79  	HTTPClient http.Client
    80  
    81  	// UserAgent represents the User-Agent header in the HTTP request.
    82  	UserAgent UserAgent
    83  
    84  	// ReauthFunc is the function used to re-authenticate the user if the request
    85  	// fails with a 401 HTTP response code. This a needed because there may be multiple
    86  	// authentication functions for different Identity service versions.
    87  	ReauthFunc func() error
    88  
    89  	// AKSKAuthOptions provides the value for AK/SK authentication, it should be nil if you use token authentication,
    90  	// Otherwise, it must have a value
    91  	AKSKAuthOptions AKSKAuthOptions
    92  
    93  	// Context is the context passed to the HTTP request.
    94  	Context context.Context
    95  
    96  	// Retry backoff func
    97  	RetryBackoffFunc RetryFunc
    98  
    99  	// MaxBackoffRetries set the maximum number of backoffs. When not set, defaults to DefaultMaxBackoffRetries
   100  	MaxBackoffRetries uint
   101  
   102  	// mut is a mutex for the client. It protects read and write access to client attributes such as getting
   103  	// and setting the TokenID.
   104  	mut *sync.RWMutex
   105  
   106  	// reauthmut is a mutex for reauthentication it attempts to ensure that only one reauthentication
   107  	// attempt happens at one time.
   108  	reauthmut *reauthlock
   109  }
   110  
   111  // reauthlock represents a set of attributes used to help in the reauthentication process.
   112  type reauthlock struct {
   113  	sync.RWMutex
   114  	reauthing bool
   115  }
   116  
   117  // AuthenticatedHeaders returns a map of HTTP headers that are common for all
   118  // authenticated service requests.
   119  func (client *ProviderClient) AuthenticatedHeaders() (m map[string]string) {
   120  	if client.reauthmut != nil {
   121  		client.reauthmut.RLock()
   122  		if client.reauthmut.reauthing {
   123  			client.reauthmut.RUnlock()
   124  			return
   125  		}
   126  		client.reauthmut.RUnlock()
   127  	}
   128  	t := client.Token()
   129  	if t == "" {
   130  		return
   131  	}
   132  	return map[string]string{"X-Auth-Token": t}
   133  }
   134  
   135  // UseTokenLock creates a mutex that is used to allow safe concurrent access to the auth token.
   136  // If the application's ProviderClient is not used concurrently, this doesn't need to be called.
   137  func (client *ProviderClient) UseTokenLock() {
   138  	client.mut = new(sync.RWMutex)
   139  	client.reauthmut = new(reauthlock)
   140  }
   141  
   142  // Token safely reads the value of the auth token from the ProviderClient. Applications should
   143  // call this method to access the token instead of the TokenID field
   144  func (client *ProviderClient) Token() string {
   145  	if client.mut != nil {
   146  		client.mut.RLock()
   147  		defer client.mut.RUnlock()
   148  	}
   149  	return client.TokenID
   150  }
   151  
   152  // SetToken safely sets the value of the auth token in the ProviderClient. Applications may
   153  // use this method in a custom ReauthFunc
   154  func (client *ProviderClient) SetToken(t string) {
   155  	if client.mut != nil {
   156  		client.mut.Lock()
   157  		defer client.mut.Unlock()
   158  	}
   159  	client.TokenID = t
   160  }
   161  
   162  // Reauthenticate calls client.ReauthFunc in a thread-safe way. If this is
   163  // called because of a 401 response, the caller may pass the previous token. In
   164  // this case, the reauthentication can be skipped if another thread has already
   165  // reauthenticated in the meantime. If no previous token is known, an empty
   166  // string should be passed instead to force unconditional reauthentication.
   167  func (client *ProviderClient) Reauthenticate(previousToken string) (err error) {
   168  	if client.ReauthFunc == nil {
   169  		return nil
   170  	}
   171  
   172  	if client.mut == nil {
   173  		return client.ReauthFunc()
   174  	}
   175  
   176  	client.mut.Lock()
   177  	defer client.mut.Unlock()
   178  
   179  	client.reauthmut.Lock()
   180  	client.reauthmut.reauthing = true
   181  	client.reauthmut.Unlock()
   182  
   183  	if previousToken == "" || client.TokenID == previousToken {
   184  		err = client.ReauthFunc()
   185  	}
   186  
   187  	client.reauthmut.Lock()
   188  	client.reauthmut.reauthing = false
   189  	client.reauthmut.Unlock()
   190  	return
   191  }
   192  
   193  // RequestOpts customizes the behavior of the provider.Request() method.
   194  type RequestOpts struct {
   195  	// JSONBody, if provided, will be encoded as JSON and used as the body of the HTTP request. The
   196  	// content type of the request will default to "application/json" unless overridden by MoreHeaders.
   197  	// It's an error to specify both a JSONBody and a RawBody.
   198  	JSONBody interface{}
   199  	// RawBody contains an io.Reader that will be consumed by the request directly. No content-type
   200  	// will be set unless one is provided explicitly by MoreHeaders.
   201  	RawBody io.Reader
   202  	// JSONResponse, if provided, will be populated with the contents of the response body parsed as
   203  	// JSON.
   204  	JSONResponse interface{}
   205  	// OkCodes contains a list of numeric HTTP status codes that should be interpreted as success. If
   206  	// the response has a different code, an error will be returned.
   207  	OkCodes []int
   208  	// MoreHeaders specifies additional HTTP headers to be provide on the request. If a header is
   209  	// provided with a blank value (""), that header will be *omitted* instead: use this to suppress
   210  	// the default Accept header or an inferred Content-Type, for example.
   211  	MoreHeaders map[string]string
   212  	// ErrorContext specifies the resource error type to return if an error is encountered.
   213  	// This lets resources override default error messages based on the response status code.
   214  	ErrorContext error
   215  	// KeepResponseBody specifies whether to keep the HTTP response body. Usually used, when the HTTP
   216  	// response body is considered for further use. Valid when JSONResponse is nil.
   217  	KeepResponseBody bool
   218  }
   219  
   220  // requestState contains temporary state for a single ProviderClient.Request() call.
   221  type requestState struct {
   222  	// This flag indicates if we have reauthenticated during this request because of a 401 response.
   223  	// It ensures that we don't reauthenticate multiple times for a single request. If we
   224  	// reauthenticate, but keep getting 401 responses with the fresh token, reauthenticating some more
   225  	// will just get us into an infinite loop.
   226  	hasReauthenticated bool
   227  	// Retry-After backoff counter, increments during each backoff call
   228  	retries uint
   229  }
   230  
   231  var applicationJSON = "application/json"
   232  
   233  func jsonMarshal(t interface{}) ([]byte, error) {
   234  	buffer := &bytes.Buffer{}
   235  	enc := json.NewEncoder(buffer)
   236  	enc.SetEscapeHTML(false)
   237  	err := enc.Encode(t)
   238  	return buffer.Bytes(), err
   239  }
   240  
   241  // Request performs an HTTP request using the ProviderClient's current HTTPClient. An authentication
   242  // header will automatically be provided.
   243  func (client *ProviderClient) Request(method, url string, options *RequestOpts) (*http.Response, error) {
   244  	return client.doRequest(method, url, options, &requestState{
   245  		hasReauthenticated: false,
   246  	})
   247  }
   248  
   249  func (client *ProviderClient) doRequest(method, url string, options *RequestOpts, state *requestState) (*http.Response, error) {
   250  	var body io.Reader
   251  	var contentType *string
   252  
   253  	// Derive the content body by either encoding an arbitrary object as JSON, or by taking a provided
   254  	// io.ReadSeeker as-is. Default the content-type to application/json.
   255  	if options.JSONBody != nil {
   256  		if options.RawBody != nil {
   257  			return nil, errors.New("Please provide only one of JSONBody or RawBody to golangsdk.Request()")
   258  		}
   259  
   260  		rendered, err := jsonMarshal(options.JSONBody)
   261  		if err != nil {
   262  			return nil, err
   263  		}
   264  
   265  		body = bytes.NewReader(rendered)
   266  		contentType = &applicationJSON
   267  	}
   268  
   269  	// Return an error, when "KeepResponseBody" is true and "JSONResponse" is not nil
   270  	if options.KeepResponseBody && options.JSONResponse != nil {
   271  		return nil, errors.New("cannot use KeepResponseBody when JSONResponse is not nil")
   272  	}
   273  
   274  	if options.RawBody != nil {
   275  		body = options.RawBody
   276  	}
   277  
   278  	// Construct the http.Request.
   279  	req, err := http.NewRequest(method, url, body)
   280  	if err != nil {
   281  		return nil, err
   282  	}
   283  	if client.Context != nil {
   284  		req = req.WithContext(client.Context)
   285  	}
   286  
   287  	// Populate the request headers. Apply options.MoreHeaders last, to give the caller the chance to
   288  	// modify or omit any header.
   289  	if contentType != nil {
   290  		req.Header.Set("Content-Type", *contentType)
   291  	}
   292  	req.Header.Set("Accept", applicationJSON)
   293  
   294  	// Set the User-Agent header
   295  	req.Header.Set("User-Agent", client.UserAgent.Join())
   296  
   297  	if options.MoreHeaders != nil {
   298  		for k, v := range options.MoreHeaders {
   299  			if v != "" {
   300  				req.Header.Set(k, v)
   301  			} else {
   302  				req.Header.Del(k)
   303  			}
   304  		}
   305  	}
   306  
   307  	// get latest token from client
   308  	for k, v := range client.AuthenticatedHeaders() {
   309  		req.Header.Set(k, v)
   310  	}
   311  
   312  	prereqtok := req.Header.Get("X-Auth-Token")
   313  
   314  	if client.AKSKAuthOptions.AccessKey != "" {
   315  		if err := auth.Sign(req, client.AKSKAuthOptions.AccessKey, client.AKSKAuthOptions.SecretKey); err != nil {
   316  			return nil, err
   317  		}
   318  		if client.AKSKAuthOptions.ProjectId != "" {
   319  			req.Header.Set("X-Project-Id", client.AKSKAuthOptions.ProjectId)
   320  		}
   321  		if client.AKSKAuthOptions.DomainID != "" {
   322  			req.Header.Set("X-Domain-Id", client.AKSKAuthOptions.DomainID)
   323  		}
   324  		if client.AKSKAuthOptions.SecurityToken != "" {
   325  			req.Header.Set("X-Security-Token", client.AKSKAuthOptions.SecurityToken)
   326  		}
   327  	}
   328  
   329  	// Issue the request.
   330  	resp, err := client.HTTPClient.Do(req)
   331  	if err != nil {
   332  		return nil, err
   333  	}
   334  
   335  	// Allow default OkCodes if none explicitly set
   336  	okc := options.OkCodes
   337  	if okc == nil {
   338  		okc = defaultOkCodes(method)
   339  	}
   340  
   341  	// Validate the HTTP response status.
   342  	var ok bool
   343  	for _, code := range okc {
   344  		if resp.StatusCode == code {
   345  			ok = true
   346  			break
   347  		}
   348  	}
   349  
   350  	if !ok {
   351  		body, _ := ioutil.ReadAll(resp.Body)
   352  		resp.Body.Close()
   353  		respErr := ErrUnexpectedResponseCode{
   354  			URL:       url,
   355  			Method:    method,
   356  			Expected:  okc,
   357  			Actual:    resp.StatusCode,
   358  			RequestId: resp.Header.Get("X-Request-Id"),
   359  			Body:      body,
   360  		}
   361  
   362  		errType := options.ErrorContext
   363  		switch resp.StatusCode {
   364  		case http.StatusBadRequest:
   365  			err = ErrDefault400{respErr}
   366  			if error400er, ok := errType.(Err400er); ok {
   367  				err = error400er.Error400(respErr)
   368  			}
   369  		case http.StatusUnauthorized:
   370  			if client.ReauthFunc != nil && !state.hasReauthenticated {
   371  				err = client.Reauthenticate(prereqtok)
   372  				if err != nil {
   373  					e := &ErrUnableToReauthenticate{}
   374  					e.ErrOriginal = respErr
   375  					return nil, e
   376  				}
   377  				if options.RawBody != nil {
   378  					if seeker, ok := options.RawBody.(io.Seeker); ok {
   379  						seeker.Seek(0, 0)
   380  					}
   381  				}
   382  				state.hasReauthenticated = true
   383  				resp, err = client.doRequest(method, url, options, state)
   384  				if err != nil {
   385  					switch err.(type) {
   386  					case *ErrUnexpectedResponseCode:
   387  						e := &ErrErrorAfterReauthentication{}
   388  						e.ErrOriginal = err.(*ErrUnexpectedResponseCode)
   389  						return nil, e
   390  					default:
   391  						e := &ErrErrorAfterReauthentication{}
   392  						e.ErrOriginal = err
   393  						return nil, e
   394  					}
   395  				}
   396  				return resp, nil
   397  			}
   398  			err = ErrDefault401{respErr}
   399  			if error401er, ok := errType.(Err401er); ok {
   400  				err = error401er.Error401(respErr)
   401  			}
   402  		case http.StatusForbidden:
   403  			err = ErrDefault403{respErr}
   404  			if error403er, ok := errType.(Err403er); ok {
   405  				err = error403er.Error403(respErr)
   406  			}
   407  		case http.StatusNotFound:
   408  			err = ErrDefault404{respErr}
   409  			if error404er, ok := errType.(Err404er); ok {
   410  				err = error404er.Error404(respErr)
   411  			}
   412  		case http.StatusMethodNotAllowed:
   413  			err = ErrDefault405{respErr}
   414  			if error405er, ok := errType.(Err405er); ok {
   415  				err = error405er.Error405(respErr)
   416  			}
   417  		case http.StatusRequestTimeout:
   418  			err = ErrDefault408{respErr}
   419  			if error408er, ok := errType.(Err408er); ok {
   420  				err = error408er.Error408(respErr)
   421  			}
   422  		case http.StatusTooManyRequests:
   423  			err = ErrDefault429{respErr}
   424  			if error429er, ok := errType.(Err429er); ok {
   425  				err = error429er.Error429(respErr)
   426  			}
   427  
   428  			maxTries := client.MaxBackoffRetries
   429  			if maxTries == 0 {
   430  				maxTries = DefaultMaxBackoffRetries
   431  			}
   432  
   433  			if f := client.RetryBackoffFunc; f != nil && state.retries < maxTries {
   434  				var e error
   435  
   436  				e = f(client.Context, &respErr, err, state.retries)
   437  				if e != nil {
   438  					return resp, e
   439  				}
   440  
   441  				state.retries = state.retries + 1
   442  				return client.doRequest(method, url, options, state)
   443  			}
   444  		case http.StatusInternalServerError:
   445  			err = ErrDefault500{respErr}
   446  			if error500er, ok := errType.(Err500er); ok {
   447  				err = error500er.Error500(respErr)
   448  			}
   449  		case http.StatusServiceUnavailable:
   450  			err = ErrDefault503{respErr}
   451  			if error503er, ok := errType.(Err503er); ok {
   452  				err = error503er.Error503(respErr)
   453  			}
   454  		}
   455  
   456  		if err == nil {
   457  			err = respErr
   458  		}
   459  
   460  		return resp, err
   461  	}
   462  
   463  	// Parse the response body as JSON, if requested to do so.
   464  	if options.JSONResponse != nil {
   465  		defer resp.Body.Close()
   466  		// Don't decode JSON when there is no content
   467  		if resp.StatusCode == http.StatusNoContent {
   468  			// read till EOF, otherwise the connection will be closed and cannot be reused
   469  			_, err = io.Copy(ioutil.Discard, resp.Body)
   470  			return resp, err
   471  		}
   472  		if err := json.NewDecoder(resp.Body).Decode(options.JSONResponse); err != nil {
   473  			return nil, err
   474  		}
   475  	}
   476  
   477  	// Close unused body to allow the HTTP connection to be reused
   478  	if !options.KeepResponseBody && options.JSONResponse == nil {
   479  		defer resp.Body.Close()
   480  		// read till EOF, otherwise the connection will be closed and cannot be reused
   481  		if _, err := io.Copy(ioutil.Discard, resp.Body); err != nil {
   482  			return nil, err
   483  		}
   484  	}
   485  
   486  	return resp, nil
   487  }
   488  
   489  func defaultOkCodes(method string) []int {
   490  	switch method {
   491  	case "GET", "HEAD":
   492  		return []int{200}
   493  	case "POST":
   494  		return []int{200, 201, 202}
   495  	case "PUT":
   496  		return []int{200, 201, 202}
   497  	case "PATCH":
   498  		return []int{200, 202, 204}
   499  	case "DELETE":
   500  		return []int{200, 202, 204}
   501  	}
   502  
   503  	return []int{}
   504  }