|
Packit |
63bb0d |
package gophercloud
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
import (
|
|
Packit |
63bb0d |
"bytes"
|
|
Packit |
63bb0d |
"context"
|
|
Packit |
63bb0d |
"encoding/json"
|
|
Packit |
63bb0d |
"errors"
|
|
Packit |
63bb0d |
"io"
|
|
Packit |
63bb0d |
"io/ioutil"
|
|
Packit |
63bb0d |
"net/http"
|
|
Packit |
63bb0d |
"strings"
|
|
Packit |
63bb0d |
"sync"
|
|
Packit |
63bb0d |
)
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
// DefaultUserAgent is the default User-Agent string set in the request header.
|
|
Packit |
63bb0d |
const DefaultUserAgent = "gophercloud/2.0.0"
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
// UserAgent represents a User-Agent header.
|
|
Packit |
63bb0d |
type UserAgent struct {
|
|
Packit |
63bb0d |
// prepend is the slice of User-Agent strings to prepend to DefaultUserAgent.
|
|
Packit |
63bb0d |
// All the strings to prepend are accumulated and prepended in the Join method.
|
|
Packit |
63bb0d |
prepend []string
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
// Prepend prepends a user-defined string to the default User-Agent string. Users
|
|
Packit |
63bb0d |
// may pass in one or more strings to prepend.
|
|
Packit |
63bb0d |
func (ua *UserAgent) Prepend(s ...string) {
|
|
Packit |
63bb0d |
ua.prepend = append(s, ua.prepend...)
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
// Join concatenates all the user-defined User-Agend strings with the default
|
|
Packit |
63bb0d |
// Gophercloud User-Agent string.
|
|
Packit |
63bb0d |
func (ua *UserAgent) Join() string {
|
|
Packit |
63bb0d |
uaSlice := append(ua.prepend, DefaultUserAgent)
|
|
Packit |
63bb0d |
return strings.Join(uaSlice, " ")
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
// ProviderClient stores details that are required to interact with any
|
|
Packit |
63bb0d |
// services within a specific provider's API.
|
|
Packit |
63bb0d |
//
|
|
Packit |
63bb0d |
// Generally, you acquire a ProviderClient by calling the NewClient method in
|
|
Packit |
63bb0d |
// the appropriate provider's child package, providing whatever authentication
|
|
Packit |
63bb0d |
// credentials are required.
|
|
Packit |
63bb0d |
type ProviderClient struct {
|
|
Packit |
63bb0d |
// IdentityBase is the base URL used for a particular provider's identity
|
|
Packit |
63bb0d |
// service - it will be used when issuing authenticatation requests. It
|
|
Packit |
63bb0d |
// should point to the root resource of the identity service, not a specific
|
|
Packit |
63bb0d |
// identity version.
|
|
Packit |
63bb0d |
IdentityBase string
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
// IdentityEndpoint is the identity endpoint. This may be a specific version
|
|
Packit |
63bb0d |
// of the identity service. If this is the case, this endpoint is used rather
|
|
Packit |
63bb0d |
// than querying versions first.
|
|
Packit |
63bb0d |
IdentityEndpoint string
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
// TokenID is the ID of the most recently issued valid token.
|
|
Packit |
63bb0d |
// NOTE: Aside from within a custom ReauthFunc, this field shouldn't be set by an application.
|
|
Packit |
63bb0d |
// To safely read or write this value, call `Token` or `SetToken`, respectively
|
|
Packit |
63bb0d |
TokenID string
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
// EndpointLocator describes how this provider discovers the endpoints for
|
|
Packit |
63bb0d |
// its constituent services.
|
|
Packit |
63bb0d |
EndpointLocator EndpointLocator
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
// HTTPClient allows users to interject arbitrary http, https, or other transit behaviors.
|
|
Packit |
63bb0d |
HTTPClient http.Client
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
// UserAgent represents the User-Agent header in the HTTP request.
|
|
Packit |
63bb0d |
UserAgent UserAgent
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
// ReauthFunc is the function used to re-authenticate the user if the request
|
|
Packit |
63bb0d |
// fails with a 401 HTTP response code. This a needed because there may be multiple
|
|
Packit |
63bb0d |
// authentication functions for different Identity service versions.
|
|
Packit |
63bb0d |
ReauthFunc func() error
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
// Throwaway determines whether if this client is a throw-away client. It's a copy of user's provider client
|
|
Packit |
63bb0d |
// with the token and reauth func zeroed. Such client can be used to perform reauthorization.
|
|
Packit |
63bb0d |
Throwaway bool
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
// Context is the context passed to the HTTP request.
|
|
Packit |
63bb0d |
Context context.Context
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
// mut is a mutex for the client. It protects read and write access to client attributes such as getting
|
|
Packit |
63bb0d |
// and setting the TokenID.
|
|
Packit |
63bb0d |
mut *sync.RWMutex
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
// reauthmut is a mutex for reauthentication it attempts to ensure that only one reauthentication
|
|
Packit |
63bb0d |
// attempt happens at one time.
|
|
Packit |
63bb0d |
reauthmut *reauthlock
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
authResult AuthResult
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
// reauthlock represents a set of attributes used to help in the reauthentication process.
|
|
Packit |
63bb0d |
type reauthlock struct {
|
|
Packit |
63bb0d |
sync.RWMutex
|
|
Packit |
63bb0d |
// This channel is non-nil during reauthentication. It can be used to ask the
|
|
Packit |
63bb0d |
// goroutine doing Reauthenticate() for its result. Look at the implementation
|
|
Packit |
63bb0d |
// of Reauthenticate() for details.
|
|
Packit |
63bb0d |
ongoing chan<- (chan<- error)
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
// AuthenticatedHeaders returns a map of HTTP headers that are common for all
|
|
Packit |
63bb0d |
// authenticated service requests. Blocks if Reauthenticate is in progress.
|
|
Packit |
63bb0d |
func (client *ProviderClient) AuthenticatedHeaders() (m map[string]string) {
|
|
Packit |
63bb0d |
if client.IsThrowaway() {
|
|
Packit |
63bb0d |
return
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
if client.reauthmut != nil {
|
|
Packit |
63bb0d |
// If a Reauthenticate is in progress, wait for it to complete.
|
|
Packit |
63bb0d |
client.reauthmut.Lock()
|
|
Packit |
63bb0d |
ongoing := client.reauthmut.ongoing
|
|
Packit |
63bb0d |
client.reauthmut.Unlock()
|
|
Packit |
63bb0d |
if ongoing != nil {
|
|
Packit |
63bb0d |
responseChannel := make(chan error)
|
|
Packit |
63bb0d |
ongoing <- responseChannel
|
|
Packit |
63bb0d |
_ = <-responseChannel
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
t := client.Token()
|
|
Packit |
63bb0d |
if t == "" {
|
|
Packit |
63bb0d |
return
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
return map[string]string{"X-Auth-Token": t}
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
// UseTokenLock creates a mutex that is used to allow safe concurrent access to the auth token.
|
|
Packit |
63bb0d |
// If the application's ProviderClient is not used concurrently, this doesn't need to be called.
|
|
Packit |
63bb0d |
func (client *ProviderClient) UseTokenLock() {
|
|
Packit |
63bb0d |
client.mut = new(sync.RWMutex)
|
|
Packit |
63bb0d |
client.reauthmut = new(reauthlock)
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
// GetAuthResult returns the result from the request that was used to obtain a
|
|
Packit |
63bb0d |
// provider client's Keystone token.
|
|
Packit |
63bb0d |
//
|
|
Packit |
63bb0d |
// The result is nil when authentication has not yet taken place, when the token
|
|
Packit |
63bb0d |
// was set manually with SetToken(), or when a ReauthFunc was used that does not
|
|
Packit |
63bb0d |
// record the AuthResult.
|
|
Packit |
63bb0d |
func (client *ProviderClient) GetAuthResult() AuthResult {
|
|
Packit |
63bb0d |
if client.mut != nil {
|
|
Packit |
63bb0d |
client.mut.RLock()
|
|
Packit |
63bb0d |
defer client.mut.RUnlock()
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
return client.authResult
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
// Token safely reads the value of the auth token from the ProviderClient. Applications should
|
|
Packit |
63bb0d |
// call this method to access the token instead of the TokenID field
|
|
Packit |
63bb0d |
func (client *ProviderClient) Token() string {
|
|
Packit |
63bb0d |
if client.mut != nil {
|
|
Packit |
63bb0d |
client.mut.RLock()
|
|
Packit |
63bb0d |
defer client.mut.RUnlock()
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
return client.TokenID
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
// SetToken safely sets the value of the auth token in the ProviderClient. Applications may
|
|
Packit |
63bb0d |
// use this method in a custom ReauthFunc.
|
|
Packit |
63bb0d |
//
|
|
Packit |
63bb0d |
// WARNING: This function is deprecated. Use SetTokenAndAuthResult() instead.
|
|
Packit |
63bb0d |
func (client *ProviderClient) SetToken(t string) {
|
|
Packit |
63bb0d |
if client.mut != nil {
|
|
Packit |
63bb0d |
client.mut.Lock()
|
|
Packit |
63bb0d |
defer client.mut.Unlock()
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
client.TokenID = t
|
|
Packit |
63bb0d |
client.authResult = nil
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
// SetTokenAndAuthResult safely sets the value of the auth token in the
|
|
Packit |
63bb0d |
// ProviderClient and also records the AuthResult that was returned from the
|
|
Packit |
63bb0d |
// token creation request. Applications may call this in a custom ReauthFunc.
|
|
Packit |
63bb0d |
func (client *ProviderClient) SetTokenAndAuthResult(r AuthResult) error {
|
|
Packit |
63bb0d |
tokenID := ""
|
|
Packit |
63bb0d |
var err error
|
|
Packit |
63bb0d |
if r != nil {
|
|
Packit |
63bb0d |
tokenID, err = r.ExtractTokenID()
|
|
Packit |
63bb0d |
if err != nil {
|
|
Packit |
63bb0d |
return err
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
if client.mut != nil {
|
|
Packit |
63bb0d |
client.mut.Lock()
|
|
Packit |
63bb0d |
defer client.mut.Unlock()
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
client.TokenID = tokenID
|
|
Packit |
63bb0d |
client.authResult = r
|
|
Packit |
63bb0d |
return nil
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
// CopyTokenFrom safely copies the token from another ProviderClient into the
|
|
Packit |
63bb0d |
// this one.
|
|
Packit |
63bb0d |
func (client *ProviderClient) CopyTokenFrom(other *ProviderClient) {
|
|
Packit |
63bb0d |
if client.mut != nil {
|
|
Packit |
63bb0d |
client.mut.Lock()
|
|
Packit |
63bb0d |
defer client.mut.Unlock()
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
if other.mut != nil && other.mut != client.mut {
|
|
Packit |
63bb0d |
other.mut.RLock()
|
|
Packit |
63bb0d |
defer other.mut.RUnlock()
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
client.TokenID = other.TokenID
|
|
Packit |
63bb0d |
client.authResult = other.authResult
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
// IsThrowaway safely reads the value of the client Throwaway field.
|
|
Packit |
63bb0d |
func (client *ProviderClient) IsThrowaway() bool {
|
|
Packit |
63bb0d |
if client.reauthmut != nil {
|
|
Packit |
63bb0d |
client.reauthmut.RLock()
|
|
Packit |
63bb0d |
defer client.reauthmut.RUnlock()
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
return client.Throwaway
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
// SetThrowaway safely sets the value of the client Throwaway field.
|
|
Packit |
63bb0d |
func (client *ProviderClient) SetThrowaway(v bool) {
|
|
Packit |
63bb0d |
if client.reauthmut != nil {
|
|
Packit |
63bb0d |
client.reauthmut.Lock()
|
|
Packit |
63bb0d |
defer client.reauthmut.Unlock()
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
client.Throwaway = v
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
// Reauthenticate calls client.ReauthFunc in a thread-safe way. If this is
|
|
Packit |
63bb0d |
// called because of a 401 response, the caller may pass the previous token. In
|
|
Packit |
63bb0d |
// this case, the reauthentication can be skipped if another thread has already
|
|
Packit |
63bb0d |
// reauthenticated in the meantime. If no previous token is known, an empty
|
|
Packit |
63bb0d |
// string should be passed instead to force unconditional reauthentication.
|
|
Packit |
63bb0d |
func (client *ProviderClient) Reauthenticate(previousToken string) error {
|
|
Packit |
63bb0d |
if client.ReauthFunc == nil {
|
|
Packit |
63bb0d |
return nil
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
if client.reauthmut == nil {
|
|
Packit |
63bb0d |
return client.ReauthFunc()
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
messages := make(chan (chan<- error))
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
// Check if a Reauthenticate is in progress, or start one if not.
|
|
Packit |
63bb0d |
client.reauthmut.Lock()
|
|
Packit |
63bb0d |
ongoing := client.reauthmut.ongoing
|
|
Packit |
63bb0d |
if ongoing == nil {
|
|
Packit |
63bb0d |
client.reauthmut.ongoing = messages
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
client.reauthmut.Unlock()
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
// If Reauthenticate is running elsewhere, wait for its result.
|
|
Packit |
63bb0d |
if ongoing != nil {
|
|
Packit |
63bb0d |
responseChannel := make(chan error)
|
|
Packit |
63bb0d |
ongoing <- responseChannel
|
|
Packit |
63bb0d |
return <-responseChannel
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
// Perform the actual reauthentication.
|
|
Packit |
63bb0d |
var err error
|
|
Packit |
63bb0d |
if previousToken == "" || client.TokenID == previousToken {
|
|
Packit |
63bb0d |
err = client.ReauthFunc()
|
|
Packit |
63bb0d |
} else {
|
|
Packit |
63bb0d |
err = nil
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
// Mark Reauthenticate as finished.
|
|
Packit |
63bb0d |
client.reauthmut.Lock()
|
|
Packit |
63bb0d |
client.reauthmut.ongoing = nil
|
|
Packit |
63bb0d |
client.reauthmut.Unlock()
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
// Report result to all other interested goroutines.
|
|
Packit |
63bb0d |
//
|
|
Packit |
63bb0d |
// This happens in a separate goroutine because another goroutine might have
|
|
Packit |
63bb0d |
// acquired a copy of `client.reauthmut.ongoing` before we cleared it, but not
|
|
Packit |
63bb0d |
// have come around to sending its request. By answering in a goroutine, we
|
|
Packit |
63bb0d |
// can have that goroutine linger until all responseChannels have been sent.
|
|
Packit |
63bb0d |
// When GC has collected all sendings ends of the channel, our receiving end
|
|
Packit |
63bb0d |
// will be closed and the goroutine will end.
|
|
Packit |
63bb0d |
go func() {
|
|
Packit |
63bb0d |
for responseChannel := range messages {
|
|
Packit |
63bb0d |
responseChannel <- err
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
}()
|
|
Packit |
63bb0d |
return err
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
// RequestOpts customizes the behavior of the provider.Request() method.
|
|
Packit |
63bb0d |
type RequestOpts struct {
|
|
Packit |
63bb0d |
// JSONBody, if provided, will be encoded as JSON and used as the body of the HTTP request. The
|
|
Packit |
63bb0d |
// content type of the request will default to "application/json" unless overridden by MoreHeaders.
|
|
Packit |
63bb0d |
// It's an error to specify both a JSONBody and a RawBody.
|
|
Packit |
63bb0d |
JSONBody interface{}
|
|
Packit |
63bb0d |
// RawBody contains an io.Reader that will be consumed by the request directly. No content-type
|
|
Packit |
63bb0d |
// will be set unless one is provided explicitly by MoreHeaders.
|
|
Packit |
63bb0d |
RawBody io.Reader
|
|
Packit |
63bb0d |
// JSONResponse, if provided, will be populated with the contents of the response body parsed as
|
|
Packit |
63bb0d |
// JSON.
|
|
Packit |
63bb0d |
JSONResponse interface{}
|
|
Packit |
63bb0d |
// OkCodes contains a list of numeric HTTP status codes that should be interpreted as success. If
|
|
Packit |
63bb0d |
// the response has a different code, an error will be returned.
|
|
Packit |
63bb0d |
OkCodes []int
|
|
Packit |
63bb0d |
// MoreHeaders specifies additional HTTP headers to be provide on the request. If a header is
|
|
Packit |
63bb0d |
// provided with a blank value (""), that header will be *omitted* instead: use this to suppress
|
|
Packit |
63bb0d |
// the default Accept header or an inferred Content-Type, for example.
|
|
Packit |
63bb0d |
MoreHeaders map[string]string
|
|
Packit |
63bb0d |
// ErrorContext specifies the resource error type to return if an error is encountered.
|
|
Packit |
63bb0d |
// This lets resources override default error messages based on the response status code.
|
|
Packit |
63bb0d |
ErrorContext error
|
|
Packit |
63bb0d |
// KeepResponseBody specifies whether to keep the HTTP response body. Usually used, when the HTTP
|
|
Packit |
63bb0d |
// response body is considered for further use. Valid when JSONResponse is nil.
|
|
Packit |
63bb0d |
KeepResponseBody bool
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
// requestState contains temporary state for a single ProviderClient.Request() call.
|
|
Packit |
63bb0d |
type requestState struct {
|
|
Packit |
63bb0d |
// This flag indicates if we have reauthenticated during this request because of a 401 response.
|
|
Packit |
63bb0d |
// It ensures that we don't reauthenticate multiple times for a single request. If we
|
|
Packit |
63bb0d |
// reauthenticate, but keep getting 401 responses with the fresh token, reauthenticating some more
|
|
Packit |
63bb0d |
// will just get us into an infinite loop.
|
|
Packit |
63bb0d |
hasReauthenticated bool
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
var applicationJSON = "application/json"
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
// Request performs an HTTP request using the ProviderClient's current HTTPClient. An authentication
|
|
Packit |
63bb0d |
// header will automatically be provided.
|
|
Packit |
63bb0d |
func (client *ProviderClient) Request(method, url string, options *RequestOpts) (*http.Response, error) {
|
|
Packit |
63bb0d |
return client.doRequest(method, url, options, &requestState{
|
|
Packit |
63bb0d |
hasReauthenticated: false,
|
|
Packit |
63bb0d |
})
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
func (client *ProviderClient) doRequest(method, url string, options *RequestOpts, state *requestState) (*http.Response, error) {
|
|
Packit |
63bb0d |
var body io.Reader
|
|
Packit |
63bb0d |
var contentType *string
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
// Derive the content body by either encoding an arbitrary object as JSON, or by taking a provided
|
|
Packit |
63bb0d |
// io.ReadSeeker as-is. Default the content-type to application/json.
|
|
Packit |
63bb0d |
if options.JSONBody != nil {
|
|
Packit |
63bb0d |
if options.RawBody != nil {
|
|
Packit |
63bb0d |
return nil, errors.New("please provide only one of JSONBody or RawBody to gophercloud.Request()")
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
rendered, err := json.Marshal(options.JSONBody)
|
|
Packit |
63bb0d |
if err != nil {
|
|
Packit |
63bb0d |
return nil, err
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
body = bytes.NewReader(rendered)
|
|
Packit |
63bb0d |
contentType = &applicationJSON
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
// Return an error, when "KeepResponseBody" is true and "JSONResponse" is not nil
|
|
Packit |
63bb0d |
if options.KeepResponseBody && options.JSONResponse != nil {
|
|
Packit |
63bb0d |
return nil, errors.New("cannot use KeepResponseBody when JSONResponse is not nil")
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
if options.RawBody != nil {
|
|
Packit |
63bb0d |
body = options.RawBody
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
// Construct the http.Request.
|
|
Packit |
63bb0d |
req, err := http.NewRequest(method, url, body)
|
|
Packit |
63bb0d |
if err != nil {
|
|
Packit |
63bb0d |
return nil, err
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
if client.Context != nil {
|
|
Packit |
63bb0d |
req = req.WithContext(client.Context)
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
// Populate the request headers. Apply options.MoreHeaders last, to give the caller the chance to
|
|
Packit |
63bb0d |
// modify or omit any header.
|
|
Packit |
63bb0d |
if contentType != nil {
|
|
Packit |
63bb0d |
req.Header.Set("Content-Type", *contentType)
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
req.Header.Set("Accept", applicationJSON)
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
// Set the User-Agent header
|
|
Packit |
63bb0d |
req.Header.Set("User-Agent", client.UserAgent.Join())
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
if options.MoreHeaders != nil {
|
|
Packit |
63bb0d |
for k, v := range options.MoreHeaders {
|
|
Packit |
63bb0d |
if v != "" {
|
|
Packit |
63bb0d |
req.Header.Set(k, v)
|
|
Packit |
63bb0d |
} else {
|
|
Packit |
63bb0d |
req.Header.Del(k)
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
// get latest token from client
|
|
Packit |
63bb0d |
for k, v := range client.AuthenticatedHeaders() {
|
|
Packit |
63bb0d |
req.Header.Set(k, v)
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
prereqtok := req.Header.Get("X-Auth-Token")
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
// Issue the request.
|
|
Packit |
63bb0d |
resp, err := client.HTTPClient.Do(req)
|
|
Packit |
63bb0d |
if err != nil {
|
|
Packit |
63bb0d |
return nil, err
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
// Allow default OkCodes if none explicitly set
|
|
Packit |
63bb0d |
okc := options.OkCodes
|
|
Packit |
63bb0d |
if okc == nil {
|
|
Packit |
63bb0d |
okc = defaultOkCodes(method)
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
// Validate the HTTP response status.
|
|
Packit |
63bb0d |
var ok bool
|
|
Packit |
63bb0d |
for _, code := range okc {
|
|
Packit |
63bb0d |
if resp.StatusCode == code {
|
|
Packit |
63bb0d |
ok = true
|
|
Packit |
63bb0d |
break
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
if !ok {
|
|
Packit |
63bb0d |
body, _ := ioutil.ReadAll(resp.Body)
|
|
Packit |
63bb0d |
resp.Body.Close()
|
|
Packit |
63bb0d |
respErr := ErrUnexpectedResponseCode{
|
|
Packit |
63bb0d |
URL: url,
|
|
Packit |
63bb0d |
Method: method,
|
|
Packit |
63bb0d |
Expected: options.OkCodes,
|
|
Packit |
63bb0d |
Actual: resp.StatusCode,
|
|
Packit |
63bb0d |
Body: body,
|
|
Packit |
63bb0d |
ResponseHeader: resp.Header,
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
errType := options.ErrorContext
|
|
Packit |
63bb0d |
switch resp.StatusCode {
|
|
Packit |
63bb0d |
case http.StatusBadRequest:
|
|
Packit |
63bb0d |
err = ErrDefault400{respErr}
|
|
Packit |
63bb0d |
if error400er, ok := errType.(Err400er); ok {
|
|
Packit |
63bb0d |
err = error400er.Error400(respErr)
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
case http.StatusUnauthorized:
|
|
Packit |
63bb0d |
if client.ReauthFunc != nil && !state.hasReauthenticated {
|
|
Packit |
63bb0d |
err = client.Reauthenticate(prereqtok)
|
|
Packit |
63bb0d |
if err != nil {
|
|
Packit |
63bb0d |
e := &ErrUnableToReauthenticate{}
|
|
Packit |
63bb0d |
e.ErrOriginal = respErr
|
|
Packit |
63bb0d |
return nil, e
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
if options.RawBody != nil {
|
|
Packit |
63bb0d |
if seeker, ok := options.RawBody.(io.Seeker); ok {
|
|
Packit |
63bb0d |
seeker.Seek(0, 0)
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
state.hasReauthenticated = true
|
|
Packit |
63bb0d |
resp, err = client.doRequest(method, url, options, state)
|
|
Packit |
63bb0d |
if err != nil {
|
|
Packit |
63bb0d |
switch err.(type) {
|
|
Packit |
63bb0d |
case *ErrUnexpectedResponseCode:
|
|
Packit |
63bb0d |
e := &ErrErrorAfterReauthentication{}
|
|
Packit |
63bb0d |
e.ErrOriginal = err.(*ErrUnexpectedResponseCode)
|
|
Packit |
63bb0d |
return nil, e
|
|
Packit |
63bb0d |
default:
|
|
Packit |
63bb0d |
e := &ErrErrorAfterReauthentication{}
|
|
Packit |
63bb0d |
e.ErrOriginal = err
|
|
Packit |
63bb0d |
return nil, e
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
return resp, nil
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
err = ErrDefault401{respErr}
|
|
Packit |
63bb0d |
if error401er, ok := errType.(Err401er); ok {
|
|
Packit |
63bb0d |
err = error401er.Error401(respErr)
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
case http.StatusForbidden:
|
|
Packit |
63bb0d |
err = ErrDefault403{respErr}
|
|
Packit |
63bb0d |
if error403er, ok := errType.(Err403er); ok {
|
|
Packit |
63bb0d |
err = error403er.Error403(respErr)
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
case http.StatusNotFound:
|
|
Packit |
63bb0d |
err = ErrDefault404{respErr}
|
|
Packit |
63bb0d |
if error404er, ok := errType.(Err404er); ok {
|
|
Packit |
63bb0d |
err = error404er.Error404(respErr)
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
case http.StatusMethodNotAllowed:
|
|
Packit |
63bb0d |
err = ErrDefault405{respErr}
|
|
Packit |
63bb0d |
if error405er, ok := errType.(Err405er); ok {
|
|
Packit |
63bb0d |
err = error405er.Error405(respErr)
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
case http.StatusRequestTimeout:
|
|
Packit |
63bb0d |
err = ErrDefault408{respErr}
|
|
Packit |
63bb0d |
if error408er, ok := errType.(Err408er); ok {
|
|
Packit |
63bb0d |
err = error408er.Error408(respErr)
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
case http.StatusConflict:
|
|
Packit |
63bb0d |
err = ErrDefault409{respErr}
|
|
Packit |
63bb0d |
if error409er, ok := errType.(Err409er); ok {
|
|
Packit |
63bb0d |
err = error409er.Error409(respErr)
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
case 429:
|
|
Packit |
63bb0d |
err = ErrDefault429{respErr}
|
|
Packit |
63bb0d |
if error429er, ok := errType.(Err429er); ok {
|
|
Packit |
63bb0d |
err = error429er.Error429(respErr)
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
case http.StatusInternalServerError:
|
|
Packit |
63bb0d |
err = ErrDefault500{respErr}
|
|
Packit |
63bb0d |
if error500er, ok := errType.(Err500er); ok {
|
|
Packit |
63bb0d |
err = error500er.Error500(respErr)
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
case http.StatusServiceUnavailable:
|
|
Packit |
63bb0d |
err = ErrDefault503{respErr}
|
|
Packit |
63bb0d |
if error503er, ok := errType.(Err503er); ok {
|
|
Packit |
63bb0d |
err = error503er.Error503(respErr)
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
if err == nil {
|
|
Packit |
63bb0d |
err = respErr
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
return resp, err
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
// Parse the response body as JSON, if requested to do so.
|
|
Packit |
63bb0d |
if options.JSONResponse != nil {
|
|
Packit |
63bb0d |
defer resp.Body.Close()
|
|
Packit |
63bb0d |
// Don't decode JSON when there is no content
|
|
Packit |
63bb0d |
if resp.StatusCode == http.StatusNoContent {
|
|
Packit |
63bb0d |
// read till EOF, otherwise the connection will be closed and cannot be reused
|
|
Packit |
63bb0d |
_, err = io.Copy(ioutil.Discard, resp.Body)
|
|
Packit |
63bb0d |
return resp, err
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
if err := json.NewDecoder(resp.Body).Decode(options.JSONResponse); err != nil {
|
|
Packit |
63bb0d |
return nil, err
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
// Close unused body to allow the HTTP connection to be reused
|
|
Packit |
63bb0d |
if !options.KeepResponseBody && options.JSONResponse == nil {
|
|
Packit |
63bb0d |
defer resp.Body.Close()
|
|
Packit |
63bb0d |
// read till EOF, otherwise the connection will be closed and cannot be reused
|
|
Packit |
63bb0d |
if _, err := io.Copy(ioutil.Discard, resp.Body); err != nil {
|
|
Packit |
63bb0d |
return nil, err
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
return resp, nil
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
func defaultOkCodes(method string) []int {
|
|
Packit |
63bb0d |
switch method {
|
|
Packit |
63bb0d |
case "GET", "HEAD":
|
|
Packit |
63bb0d |
return []int{200}
|
|
Packit |
63bb0d |
case "POST":
|
|
Packit |
63bb0d |
return []int{201, 202}
|
|
Packit |
63bb0d |
case "PUT":
|
|
Packit |
63bb0d |
return []int{201, 202}
|
|
Packit |
63bb0d |
case "PATCH":
|
|
Packit |
63bb0d |
return []int{200, 202, 204}
|
|
Packit |
63bb0d |
case "DELETE":
|
|
Packit |
63bb0d |
return []int{202, 204}
|
|
Packit |
63bb0d |
}
|
|
Packit |
63bb0d |
|
|
Packit |
63bb0d |
return []int{}
|
|
Packit |
63bb0d |
}
|