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	reauthing    bool
98	reauthingErr error
99	done         *sync.Cond
100}
101
102// AuthenticatedHeaders returns a map of HTTP headers that are common for all
103// authenticated service requests. Blocks if Reauthenticate is in progress.
104func (client *ProviderClient) AuthenticatedHeaders() (m map[string]string) {
105	if client.IsThrowaway() {
106		return
107	}
108	if client.reauthmut != nil {
109		client.reauthmut.Lock()
110		for client.reauthmut.reauthing {
111			client.reauthmut.done.Wait()
112		}
113		client.reauthmut.Unlock()
114	}
115	t := client.Token()
116	if t == "" {
117		return
118	}
119	return map[string]string{"X-Auth-Token": t}
120}
121
122// UseTokenLock creates a mutex that is used to allow safe concurrent access to the auth token.
123// If the application's ProviderClient is not used concurrently, this doesn't need to be called.
124func (client *ProviderClient) UseTokenLock() {
125	client.mut = new(sync.RWMutex)
126	client.reauthmut = new(reauthlock)
127}
128
129// GetAuthResult returns the result from the request that was used to obtain a
130// provider client's Keystone token.
131//
132// The result is nil when authentication has not yet taken place, when the token
133// was set manually with SetToken(), or when a ReauthFunc was used that does not
134// record the AuthResult.
135func (client *ProviderClient) GetAuthResult() AuthResult {
136	if client.mut != nil {
137		client.mut.RLock()
138		defer client.mut.RUnlock()
139	}
140	return client.authResult
141}
142
143// Token safely reads the value of the auth token from the ProviderClient. Applications should
144// call this method to access the token instead of the TokenID field
145func (client *ProviderClient) Token() string {
146	if client.mut != nil {
147		client.mut.RLock()
148		defer client.mut.RUnlock()
149	}
150	return client.TokenID
151}
152
153// SetToken safely sets the value of the auth token in the ProviderClient. Applications may
154// use this method in a custom ReauthFunc.
155//
156// WARNING: This function is deprecated. Use SetTokenAndAuthResult() instead.
157func (client *ProviderClient) SetToken(t string) {
158	if client.mut != nil {
159		client.mut.Lock()
160		defer client.mut.Unlock()
161	}
162	client.TokenID = t
163	client.authResult = nil
164}
165
166// SetTokenAndAuthResult safely sets the value of the auth token in the
167// ProviderClient and also records the AuthResult that was returned from the
168// token creation request. Applications may call this in a custom ReauthFunc.
169func (client *ProviderClient) SetTokenAndAuthResult(r AuthResult) error {
170	tokenID := ""
171	var err error
172	if r != nil {
173		tokenID, err = r.ExtractTokenID()
174		if err != nil {
175			return err
176		}
177	}
178
179	if client.mut != nil {
180		client.mut.Lock()
181		defer client.mut.Unlock()
182	}
183	client.TokenID = tokenID
184	client.authResult = r
185	return nil
186}
187
188// CopyTokenFrom safely copies the token from another ProviderClient into the
189// this one.
190func (client *ProviderClient) CopyTokenFrom(other *ProviderClient) {
191	if client.mut != nil {
192		client.mut.Lock()
193		defer client.mut.Unlock()
194	}
195	if other.mut != nil && other.mut != client.mut {
196		other.mut.RLock()
197		defer other.mut.RUnlock()
198	}
199	client.TokenID = other.TokenID
200	client.authResult = other.authResult
201}
202
203// IsThrowaway safely reads the value of the client Throwaway field.
204func (client *ProviderClient) IsThrowaway() bool {
205	if client.reauthmut != nil {
206		client.reauthmut.RLock()
207		defer client.reauthmut.RUnlock()
208	}
209	return client.Throwaway
210}
211
212// SetThrowaway safely sets the value of the client Throwaway field.
213func (client *ProviderClient) SetThrowaway(v bool) {
214	if client.reauthmut != nil {
215		client.reauthmut.Lock()
216		defer client.reauthmut.Unlock()
217	}
218	client.Throwaway = v
219}
220
221// Reauthenticate calls client.ReauthFunc in a thread-safe way. If this is
222// called because of a 401 response, the caller may pass the previous token. In
223// this case, the reauthentication can be skipped if another thread has already
224// reauthenticated in the meantime. If no previous token is known, an empty
225// string should be passed instead to force unconditional reauthentication.
226func (client *ProviderClient) Reauthenticate(previousToken string) (err error) {
227	if client.ReauthFunc == nil {
228		return nil
229	}
230
231	if client.reauthmut == nil {
232		return client.ReauthFunc()
233	}
234
235	client.reauthmut.Lock()
236	if client.reauthmut.reauthing {
237		for !client.reauthmut.reauthing {
238			client.reauthmut.done.Wait()
239		}
240		err = client.reauthmut.reauthingErr
241		client.reauthmut.Unlock()
242		return err
243	}
244	client.reauthmut.Unlock()
245
246	client.reauthmut.Lock()
247	client.reauthmut.reauthing = true
248	client.reauthmut.done = sync.NewCond(client.reauthmut)
249	client.reauthmut.reauthingErr = nil
250	client.reauthmut.Unlock()
251
252	if previousToken == "" || client.TokenID == previousToken {
253		err = client.ReauthFunc()
254	}
255
256	client.reauthmut.Lock()
257	client.reauthmut.reauthing = false
258	client.reauthmut.reauthingErr = err
259	client.reauthmut.done.Broadcast()
260	client.reauthmut.Unlock()
261	return
262}
263
264// RequestOpts customizes the behavior of the provider.Request() method.
265type RequestOpts struct {
266	// JSONBody, if provided, will be encoded as JSON and used as the body of the HTTP request. The
267	// content type of the request will default to "application/json" unless overridden by MoreHeaders.
268	// It's an error to specify both a JSONBody and a RawBody.
269	JSONBody interface{}
270	// RawBody contains an io.Reader that will be consumed by the request directly. No content-type
271	// will be set unless one is provided explicitly by MoreHeaders.
272	RawBody io.Reader
273	// JSONResponse, if provided, will be populated with the contents of the response body parsed as
274	// JSON.
275	JSONResponse interface{}
276	// OkCodes contains a list of numeric HTTP status codes that should be interpreted as success. If
277	// the response has a different code, an error will be returned.
278	OkCodes []int
279	// MoreHeaders specifies additional HTTP headers to be provide on the request. If a header is
280	// provided with a blank value (""), that header will be *omitted* instead: use this to suppress
281	// the default Accept header or an inferred Content-Type, for example.
282	MoreHeaders map[string]string
283	// ErrorContext specifies the resource error type to return if an error is encountered.
284	// This lets resources override default error messages based on the response status code.
285	ErrorContext error
286}
287
288var applicationJSON = "application/json"
289
290// Request performs an HTTP request using the ProviderClient's current HTTPClient. An authentication
291// header will automatically be provided.
292func (client *ProviderClient) Request(method, url string, options *RequestOpts) (*http.Response, error) {
293	var body io.Reader
294	var contentType *string
295
296	// Derive the content body by either encoding an arbitrary object as JSON, or by taking a provided
297	// io.ReadSeeker as-is. Default the content-type to application/json.
298	if options.JSONBody != nil {
299		if options.RawBody != nil {
300			return nil, errors.New("please provide only one of JSONBody or RawBody to gophercloud.Request()")
301		}
302
303		rendered, err := json.Marshal(options.JSONBody)
304		if err != nil {
305			return nil, err
306		}
307
308		body = bytes.NewReader(rendered)
309		contentType = &applicationJSON
310	}
311
312	if options.RawBody != nil {
313		body = options.RawBody
314	}
315
316	// Construct the http.Request.
317	req, err := http.NewRequest(method, url, body)
318	if err != nil {
319		return nil, err
320	}
321	if client.Context != nil {
322		req = req.WithContext(client.Context)
323	}
324
325	// Populate the request headers. Apply options.MoreHeaders last, to give the caller the chance to
326	// modify or omit any header.
327	if contentType != nil {
328		req.Header.Set("Content-Type", *contentType)
329	}
330	req.Header.Set("Accept", applicationJSON)
331
332	// Set the User-Agent header
333	req.Header.Set("User-Agent", client.UserAgent.Join())
334
335	if options.MoreHeaders != nil {
336		for k, v := range options.MoreHeaders {
337			if v != "" {
338				req.Header.Set(k, v)
339			} else {
340				req.Header.Del(k)
341			}
342		}
343	}
344
345	// get latest token from client
346	for k, v := range client.AuthenticatedHeaders() {
347		req.Header.Set(k, v)
348	}
349
350	// Set connection parameter to close the connection immediately when we've got the response
351	req.Close = true
352
353	prereqtok := req.Header.Get("X-Auth-Token")
354
355	// Issue the request.
356	resp, err := client.HTTPClient.Do(req)
357	if err != nil {
358		return nil, err
359	}
360
361	// Allow default OkCodes if none explicitly set
362	okc := options.OkCodes
363	if okc == nil {
364		okc = defaultOkCodes(method)
365	}
366
367	// Validate the HTTP response status.
368	var ok bool
369	for _, code := range okc {
370		if resp.StatusCode == code {
371			ok = true
372			break
373		}
374	}
375
376	if !ok {
377		body, _ := ioutil.ReadAll(resp.Body)
378		resp.Body.Close()
379		respErr := ErrUnexpectedResponseCode{
380			URL:      url,
381			Method:   method,
382			Expected: options.OkCodes,
383			Actual:   resp.StatusCode,
384			Body:     body,
385		}
386
387		errType := options.ErrorContext
388		switch resp.StatusCode {
389		case http.StatusBadRequest:
390			err = ErrDefault400{respErr}
391			if error400er, ok := errType.(Err400er); ok {
392				err = error400er.Error400(respErr)
393			}
394		case http.StatusUnauthorized:
395			if client.ReauthFunc != nil {
396				err = client.Reauthenticate(prereqtok)
397				if err != nil {
398					e := &ErrUnableToReauthenticate{}
399					e.ErrOriginal = respErr
400					return nil, e
401				}
402				if options.RawBody != nil {
403					if seeker, ok := options.RawBody.(io.Seeker); ok {
404						seeker.Seek(0, 0)
405					}
406				}
407				resp, err = client.Request(method, url, options)
408				if err != nil {
409					switch err.(type) {
410					case *ErrUnexpectedResponseCode:
411						e := &ErrErrorAfterReauthentication{}
412						e.ErrOriginal = err.(*ErrUnexpectedResponseCode)
413						return nil, e
414					default:
415						e := &ErrErrorAfterReauthentication{}
416						e.ErrOriginal = err
417						return nil, e
418					}
419				}
420				return resp, nil
421			}
422			err = ErrDefault401{respErr}
423			if error401er, ok := errType.(Err401er); ok {
424				err = error401er.Error401(respErr)
425			}
426		case http.StatusForbidden:
427			err = ErrDefault403{respErr}
428			if error403er, ok := errType.(Err403er); ok {
429				err = error403er.Error403(respErr)
430			}
431		case http.StatusNotFound:
432			err = ErrDefault404{respErr}
433			if error404er, ok := errType.(Err404er); ok {
434				err = error404er.Error404(respErr)
435			}
436		case http.StatusMethodNotAllowed:
437			err = ErrDefault405{respErr}
438			if error405er, ok := errType.(Err405er); ok {
439				err = error405er.Error405(respErr)
440			}
441		case http.StatusRequestTimeout:
442			err = ErrDefault408{respErr}
443			if error408er, ok := errType.(Err408er); ok {
444				err = error408er.Error408(respErr)
445			}
446		case http.StatusConflict:
447			err = ErrDefault409{respErr}
448			if error409er, ok := errType.(Err409er); ok {
449				err = error409er.Error409(respErr)
450			}
451		case 429:
452			err = ErrDefault429{respErr}
453			if error429er, ok := errType.(Err429er); ok {
454				err = error429er.Error429(respErr)
455			}
456		case http.StatusInternalServerError:
457			err = ErrDefault500{respErr}
458			if error500er, ok := errType.(Err500er); ok {
459				err = error500er.Error500(respErr)
460			}
461		case http.StatusServiceUnavailable:
462			err = ErrDefault503{respErr}
463			if error503er, ok := errType.(Err503er); ok {
464				err = error503er.Error503(respErr)
465			}
466		}
467
468		if err == nil {
469			err = respErr
470		}
471
472		return resp, err
473	}
474
475	// Parse the response body as JSON, if requested to do so.
476	if options.JSONResponse != nil {
477		defer resp.Body.Close()
478		if err := json.NewDecoder(resp.Body).Decode(options.JSONResponse); err != nil {
479			return nil, err
480		}
481	}
482
483	return resp, nil
484}
485
486func defaultOkCodes(method string) []int {
487	switch {
488	case method == "GET":
489		return []int{200}
490	case method == "POST":
491		return []int{201, 202}
492	case method == "PUT":
493		return []int{201, 202}
494	case method == "PATCH":
495		return []int{200, 202, 204}
496	case method == "DELETE":
497		return []int{202, 204}
498	}
499
500	return []int{}
501}
502