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