1package gophercloud
2
3import (
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.
16const (
17	DefaultUserAgent         = "gophercloud/2.0.0"
18	DefaultMaxBackoffRetries = 60
19)
20
21// UserAgent represents a User-Agent header.
22type 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
28type 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).
34type 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.
38func (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.
44func (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.
55type 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.
116type 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.
124type reauthFuture struct {
125	done chan struct{}
126	err  error
127}
128
129func newReauthFuture() *reauthFuture {
130	return &reauthFuture{
131		make(chan struct{}),
132		nil,
133	}
134}
135
136func (f *reauthFuture) Set(err error) {
137	f.err = err
138	close(f.done)
139}
140
141func (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.
148func (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.
170func (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.
181func (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
191func (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.
203func (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.
215func (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.
236func (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.
250func (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.
259func (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.
272func (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.
314type 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 provide on the request. If a header is
329	// provided with a blank value (""), that header will be *omitted* instead: use this to suppress
330	// the default Accept header or an inferred Content-Type, for example.
331	MoreHeaders map[string]string
332	// ErrorContext specifies the resource error type to return if an error is encountered.
333	// This lets resources override default error messages based on the response status code.
334	ErrorContext error
335	// KeepResponseBody specifies whether to keep the HTTP response body. Usually used, when the HTTP
336	// response body is considered for further use. Valid when JSONResponse is nil.
337	KeepResponseBody bool
338}
339
340// requestState contains temporary state for a single ProviderClient.Request() call.
341type requestState struct {
342	// This flag indicates if we have reauthenticated during this request because of a 401 response.
343	// It ensures that we don't reauthenticate multiple times for a single request. If we
344	// reauthenticate, but keep getting 401 responses with the fresh token, reauthenticating some more
345	// will just get us into an infinite loop.
346	hasReauthenticated bool
347	// Retry-After backoff counter, increments during each backoff call
348	retries uint
349}
350
351var applicationJSON = "application/json"
352
353// Request performs an HTTP request using the ProviderClient's current HTTPClient. An authentication
354// header will automatically be provided.
355func (client *ProviderClient) Request(method, url string, options *RequestOpts) (*http.Response, error) {
356	return client.doRequest(method, url, options, &requestState{
357		hasReauthenticated: false,
358	})
359}
360
361func (client *ProviderClient) doRequest(method, url string, options *RequestOpts, state *requestState) (*http.Response, error) {
362	var body io.Reader
363	var contentType *string
364
365	// Derive the content body by either encoding an arbitrary object as JSON, or by taking a provided
366	// io.ReadSeeker as-is. Default the content-type to application/json.
367	if options.JSONBody != nil {
368		if options.RawBody != nil {
369			return nil, errors.New("please provide only one of JSONBody or RawBody to gophercloud.Request()")
370		}
371
372		rendered, err := json.Marshal(options.JSONBody)
373		if err != nil {
374			return nil, err
375		}
376
377		body = bytes.NewReader(rendered)
378		contentType = &applicationJSON
379	}
380
381	// Return an error, when "KeepResponseBody" is true and "JSONResponse" is not nil
382	if options.KeepResponseBody && options.JSONResponse != nil {
383		return nil, errors.New("cannot use KeepResponseBody when JSONResponse is not nil")
384	}
385
386	if options.RawBody != nil {
387		body = options.RawBody
388	}
389
390	// Construct the http.Request.
391	req, err := http.NewRequest(method, url, body)
392	if err != nil {
393		return nil, err
394	}
395	if client.Context != nil {
396		req = req.WithContext(client.Context)
397	}
398
399	// Populate the request headers. Apply options.MoreHeaders last, to give the caller the chance to
400	// modify or omit any header.
401	if contentType != nil {
402		req.Header.Set("Content-Type", *contentType)
403	}
404	req.Header.Set("Accept", applicationJSON)
405
406	// Set the User-Agent header
407	req.Header.Set("User-Agent", client.UserAgent.Join())
408
409	if options.MoreHeaders != nil {
410		for k, v := range options.MoreHeaders {
411			if v != "" {
412				req.Header.Set(k, v)
413			} else {
414				req.Header.Del(k)
415			}
416		}
417	}
418
419	// get latest token from client
420	for k, v := range client.AuthenticatedHeaders() {
421		req.Header.Set(k, v)
422	}
423
424	prereqtok := req.Header.Get("X-Auth-Token")
425
426	// Issue the request.
427	resp, err := client.HTTPClient.Do(req)
428	if err != nil {
429		if client.RetryFunc != nil {
430			var e error
431			state.retries = state.retries + 1
432			e = client.RetryFunc(client.Context, method, url, options, err, state.retries)
433			if e != nil {
434				return nil, e
435			}
436
437			return client.doRequest(method, url, options, state)
438		}
439		return nil, err
440	}
441
442	// Allow default OkCodes if none explicitly set
443	okc := options.OkCodes
444	if okc == nil {
445		okc = defaultOkCodes(method)
446	}
447
448	// Validate the HTTP response status.
449	var ok bool
450	for _, code := range okc {
451		if resp.StatusCode == code {
452			ok = true
453			break
454		}
455	}
456
457	if !ok {
458		body, _ := ioutil.ReadAll(resp.Body)
459		resp.Body.Close()
460		respErr := ErrUnexpectedResponseCode{
461			URL:            url,
462			Method:         method,
463			Expected:       okc,
464			Actual:         resp.StatusCode,
465			Body:           body,
466			ResponseHeader: resp.Header,
467		}
468
469		errType := options.ErrorContext
470		switch resp.StatusCode {
471		case http.StatusBadRequest:
472			err = ErrDefault400{respErr}
473			if error400er, ok := errType.(Err400er); ok {
474				err = error400er.Error400(respErr)
475			}
476		case http.StatusUnauthorized:
477			if client.ReauthFunc != nil && !state.hasReauthenticated {
478				err = client.Reauthenticate(prereqtok)
479				if err != nil {
480					e := &ErrUnableToReauthenticate{}
481					e.ErrOriginal = respErr
482					return nil, e
483				}
484				if options.RawBody != nil {
485					if seeker, ok := options.RawBody.(io.Seeker); ok {
486						seeker.Seek(0, 0)
487					}
488				}
489				state.hasReauthenticated = true
490				resp, err = client.doRequest(method, url, options, state)
491				if err != nil {
492					switch err.(type) {
493					case *ErrUnexpectedResponseCode:
494						e := &ErrErrorAfterReauthentication{}
495						e.ErrOriginal = err.(*ErrUnexpectedResponseCode)
496						return nil, e
497					default:
498						e := &ErrErrorAfterReauthentication{}
499						e.ErrOriginal = err
500						return nil, e
501					}
502				}
503				return resp, nil
504			}
505			err = ErrDefault401{respErr}
506			if error401er, ok := errType.(Err401er); ok {
507				err = error401er.Error401(respErr)
508			}
509		case http.StatusForbidden:
510			err = ErrDefault403{respErr}
511			if error403er, ok := errType.(Err403er); ok {
512				err = error403er.Error403(respErr)
513			}
514		case http.StatusNotFound:
515			err = ErrDefault404{respErr}
516			if error404er, ok := errType.(Err404er); ok {
517				err = error404er.Error404(respErr)
518			}
519		case http.StatusMethodNotAllowed:
520			err = ErrDefault405{respErr}
521			if error405er, ok := errType.(Err405er); ok {
522				err = error405er.Error405(respErr)
523			}
524		case http.StatusRequestTimeout:
525			err = ErrDefault408{respErr}
526			if error408er, ok := errType.(Err408er); ok {
527				err = error408er.Error408(respErr)
528			}
529		case http.StatusConflict:
530			err = ErrDefault409{respErr}
531			if error409er, ok := errType.(Err409er); ok {
532				err = error409er.Error409(respErr)
533			}
534		case http.StatusTooManyRequests, 498:
535			err = ErrDefault429{respErr}
536			if error429er, ok := errType.(Err429er); ok {
537				err = error429er.Error429(respErr)
538			}
539
540			maxTries := client.MaxBackoffRetries
541			if maxTries == 0 {
542				maxTries = DefaultMaxBackoffRetries
543			}
544
545			if f := client.RetryBackoffFunc; f != nil && state.retries < maxTries {
546				var e error
547
548				state.retries = state.retries + 1
549				e = f(client.Context, &respErr, err, state.retries)
550
551				if e != nil {
552					return resp, e
553				}
554
555				return client.doRequest(method, url, options, state)
556			}
557		case http.StatusInternalServerError:
558			err = ErrDefault500{respErr}
559			if error500er, ok := errType.(Err500er); ok {
560				err = error500er.Error500(respErr)
561			}
562		case http.StatusServiceUnavailable:
563			err = ErrDefault503{respErr}
564			if error503er, ok := errType.(Err503er); ok {
565				err = error503er.Error503(respErr)
566			}
567		}
568
569		if err == nil {
570			err = respErr
571		}
572
573		if err != nil && client.RetryFunc != nil {
574			var e error
575			state.retries = state.retries + 1
576			e = client.RetryFunc(client.Context, method, url, options, err, state.retries)
577			if e != nil {
578				return resp, e
579			}
580
581			return client.doRequest(method, url, options, state)
582		}
583
584		return resp, err
585	}
586
587	// Parse the response body as JSON, if requested to do so.
588	if options.JSONResponse != nil {
589		defer resp.Body.Close()
590		// Don't decode JSON when there is no content
591		if resp.StatusCode == http.StatusNoContent {
592			// read till EOF, otherwise the connection will be closed and cannot be reused
593			_, err = io.Copy(ioutil.Discard, resp.Body)
594			return resp, err
595		}
596		if err := json.NewDecoder(resp.Body).Decode(options.JSONResponse); err != nil {
597			if client.RetryFunc != nil {
598				var e error
599				state.retries = state.retries + 1
600				e = client.RetryFunc(client.Context, method, url, options, err, state.retries)
601				if e != nil {
602					return resp, e
603				}
604
605				return client.doRequest(method, url, options, state)
606			}
607			return nil, err
608		}
609	}
610
611	// Close unused body to allow the HTTP connection to be reused
612	if !options.KeepResponseBody && options.JSONResponse == nil {
613		defer resp.Body.Close()
614		// read till EOF, otherwise the connection will be closed and cannot be reused
615		if _, err := io.Copy(ioutil.Discard, resp.Body); err != nil {
616			return nil, err
617		}
618	}
619
620	return resp, nil
621}
622
623func defaultOkCodes(method string) []int {
624	switch method {
625	case "GET", "HEAD":
626		return []int{200}
627	case "POST":
628		return []int{201, 202}
629	case "PUT":
630		return []int{201, 202}
631	case "PATCH":
632		return []int{200, 202, 204}
633	case "DELETE":
634		return []int{202, 204}
635	}
636
637	return []int{}
638}
639