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