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	// KeepResponseBody specifies whether to keep the HTTP response body. Usually used, when the HTTP
309	// response body is considered for further use. Valid when JSONResponse is nil.
310	KeepResponseBody bool
311}
312
313// requestState contains temporary state for a single ProviderClient.Request() call.
314type requestState struct {
315	// This flag indicates if we have reauthenticated during this request because of a 401 response.
316	// It ensures that we don't reauthenticate multiple times for a single request. If we
317	// reauthenticate, but keep getting 401 responses with the fresh token, reauthenticating some more
318	// will just get us into an infinite loop.
319	hasReauthenticated bool
320}
321
322var applicationJSON = "application/json"
323
324// Request performs an HTTP request using the ProviderClient's current HTTPClient. An authentication
325// header will automatically be provided.
326func (client *ProviderClient) Request(method, url string, options *RequestOpts) (*http.Response, error) {
327	return client.doRequest(method, url, options, &requestState{
328		hasReauthenticated: false,
329	})
330}
331
332func (client *ProviderClient) doRequest(method, url string, options *RequestOpts, state *requestState) (*http.Response, error) {
333	var body io.Reader
334	var contentType *string
335
336	// Derive the content body by either encoding an arbitrary object as JSON, or by taking a provided
337	// io.ReadSeeker as-is. Default the content-type to application/json.
338	if options.JSONBody != nil {
339		if options.RawBody != nil {
340			return nil, errors.New("please provide only one of JSONBody or RawBody to gophercloud.Request()")
341		}
342
343		rendered, err := json.Marshal(options.JSONBody)
344		if err != nil {
345			return nil, err
346		}
347
348		body = bytes.NewReader(rendered)
349		contentType = &applicationJSON
350	}
351
352	// Return an error, when "KeepResponseBody" is true and "JSONResponse" is not nil
353	if options.KeepResponseBody && options.JSONResponse != nil {
354		return nil, errors.New("cannot use KeepResponseBody when JSONResponse is not nil")
355	}
356
357	if options.RawBody != nil {
358		body = options.RawBody
359	}
360
361	// Construct the http.Request.
362	req, err := http.NewRequest(method, url, body)
363	if err != nil {
364		return nil, err
365	}
366	if client.Context != nil {
367		req = req.WithContext(client.Context)
368	}
369
370	// Populate the request headers. Apply options.MoreHeaders last, to give the caller the chance to
371	// modify or omit any header.
372	if contentType != nil {
373		req.Header.Set("Content-Type", *contentType)
374	}
375	req.Header.Set("Accept", applicationJSON)
376
377	// Set the User-Agent header
378	req.Header.Set("User-Agent", client.UserAgent.Join())
379
380	if options.MoreHeaders != nil {
381		for k, v := range options.MoreHeaders {
382			if v != "" {
383				req.Header.Set(k, v)
384			} else {
385				req.Header.Del(k)
386			}
387		}
388	}
389
390	// get latest token from client
391	for k, v := range client.AuthenticatedHeaders() {
392		req.Header.Set(k, v)
393	}
394
395	prereqtok := req.Header.Get("X-Auth-Token")
396
397	// Issue the request.
398	resp, err := client.HTTPClient.Do(req)
399	if err != nil {
400		return nil, err
401	}
402
403	// Allow default OkCodes if none explicitly set
404	okc := options.OkCodes
405	if okc == nil {
406		okc = defaultOkCodes(method)
407	}
408
409	// Validate the HTTP response status.
410	var ok bool
411	for _, code := range okc {
412		if resp.StatusCode == code {
413			ok = true
414			break
415		}
416	}
417
418	if !ok {
419		body, _ := ioutil.ReadAll(resp.Body)
420		resp.Body.Close()
421		respErr := ErrUnexpectedResponseCode{
422			URL:            url,
423			Method:         method,
424			Expected:       options.OkCodes,
425			Actual:         resp.StatusCode,
426			Body:           body,
427			ResponseHeader: resp.Header,
428		}
429
430		errType := options.ErrorContext
431		switch resp.StatusCode {
432		case http.StatusBadRequest:
433			err = ErrDefault400{respErr}
434			if error400er, ok := errType.(Err400er); ok {
435				err = error400er.Error400(respErr)
436			}
437		case http.StatusUnauthorized:
438			if client.ReauthFunc != nil && !state.hasReauthenticated {
439				err = client.Reauthenticate(prereqtok)
440				if err != nil {
441					e := &ErrUnableToReauthenticate{}
442					e.ErrOriginal = respErr
443					return nil, e
444				}
445				if options.RawBody != nil {
446					if seeker, ok := options.RawBody.(io.Seeker); ok {
447						seeker.Seek(0, 0)
448					}
449				}
450				state.hasReauthenticated = true
451				resp, err = client.doRequest(method, url, options, state)
452				if err != nil {
453					switch err.(type) {
454					case *ErrUnexpectedResponseCode:
455						e := &ErrErrorAfterReauthentication{}
456						e.ErrOriginal = err.(*ErrUnexpectedResponseCode)
457						return nil, e
458					default:
459						e := &ErrErrorAfterReauthentication{}
460						e.ErrOriginal = err
461						return nil, e
462					}
463				}
464				return resp, nil
465			}
466			err = ErrDefault401{respErr}
467			if error401er, ok := errType.(Err401er); ok {
468				err = error401er.Error401(respErr)
469			}
470		case http.StatusForbidden:
471			err = ErrDefault403{respErr}
472			if error403er, ok := errType.(Err403er); ok {
473				err = error403er.Error403(respErr)
474			}
475		case http.StatusNotFound:
476			err = ErrDefault404{respErr}
477			if error404er, ok := errType.(Err404er); ok {
478				err = error404er.Error404(respErr)
479			}
480		case http.StatusMethodNotAllowed:
481			err = ErrDefault405{respErr}
482			if error405er, ok := errType.(Err405er); ok {
483				err = error405er.Error405(respErr)
484			}
485		case http.StatusRequestTimeout:
486			err = ErrDefault408{respErr}
487			if error408er, ok := errType.(Err408er); ok {
488				err = error408er.Error408(respErr)
489			}
490		case http.StatusConflict:
491			err = ErrDefault409{respErr}
492			if error409er, ok := errType.(Err409er); ok {
493				err = error409er.Error409(respErr)
494			}
495		case 429:
496			err = ErrDefault429{respErr}
497			if error429er, ok := errType.(Err429er); ok {
498				err = error429er.Error429(respErr)
499			}
500		case http.StatusInternalServerError:
501			err = ErrDefault500{respErr}
502			if error500er, ok := errType.(Err500er); ok {
503				err = error500er.Error500(respErr)
504			}
505		case http.StatusServiceUnavailable:
506			err = ErrDefault503{respErr}
507			if error503er, ok := errType.(Err503er); ok {
508				err = error503er.Error503(respErr)
509			}
510		}
511
512		if err == nil {
513			err = respErr
514		}
515
516		return resp, err
517	}
518
519	// Parse the response body as JSON, if requested to do so.
520	if options.JSONResponse != nil {
521		defer resp.Body.Close()
522		// Don't decode JSON when there is no content
523		if resp.StatusCode == http.StatusNoContent {
524			// read till EOF, otherwise the connection will be closed and cannot be reused
525			_, err = io.Copy(ioutil.Discard, resp.Body)
526			return resp, err
527		}
528		if err := json.NewDecoder(resp.Body).Decode(options.JSONResponse); err != nil {
529			return nil, err
530		}
531	}
532
533	// Close unused body to allow the HTTP connection to be reused
534	if !options.KeepResponseBody && options.JSONResponse == nil {
535		defer resp.Body.Close()
536		// read till EOF, otherwise the connection will be closed and cannot be reused
537		if _, err := io.Copy(ioutil.Discard, resp.Body); err != nil {
538			return nil, err
539		}
540	}
541
542	return resp, nil
543}
544
545func defaultOkCodes(method string) []int {
546	switch method {
547	case "GET", "HEAD":
548		return []int{200}
549	case "POST":
550		return []int{201, 202}
551	case "PUT":
552		return []int{201, 202}
553	case "PATCH":
554		return []int{200, 202, 204}
555	case "DELETE":
556		return []int{202, 204}
557	}
558
559	return []int{}
560}
561