1package gophercloud
2
3import (
4	"bytes"
5	"encoding/json"
6	"io"
7	"io/ioutil"
8	"net/http"
9	"strings"
10	"sync"
11)
12
13// DefaultUserAgent is the default User-Agent string set in the request header.
14const DefaultUserAgent = "gophercloud/2.0.0"
15
16// UserAgent represents a User-Agent header.
17type UserAgent struct {
18	// prepend is the slice of User-Agent strings to prepend to DefaultUserAgent.
19	// All the strings to prepend are accumulated and prepended in the Join method.
20	prepend []string
21}
22
23// Prepend prepends a user-defined string to the default User-Agent string. Users
24// may pass in one or more strings to prepend.
25func (ua *UserAgent) Prepend(s ...string) {
26	ua.prepend = append(s, ua.prepend...)
27}
28
29// Join concatenates all the user-defined User-Agend strings with the default
30// Gophercloud User-Agent string.
31func (ua *UserAgent) Join() string {
32	uaSlice := append(ua.prepend, DefaultUserAgent)
33	return strings.Join(uaSlice, " ")
34}
35
36// ProviderClient stores details that are required to interact with any
37// services within a specific provider's API.
38//
39// Generally, you acquire a ProviderClient by calling the NewClient method in
40// the appropriate provider's child package, providing whatever authentication
41// credentials are required.
42type ProviderClient struct {
43	// IdentityBase is the base URL used for a particular provider's identity
44	// service - it will be used when issuing authenticatation requests. It
45	// should point to the root resource of the identity service, not a specific
46	// identity version.
47	IdentityBase string
48
49	// IdentityEndpoint is the identity endpoint. This may be a specific version
50	// of the identity service. If this is the case, this endpoint is used rather
51	// than querying versions first.
52	IdentityEndpoint string
53
54	// TokenID is the ID of the most recently issued valid token.
55	// NOTE: Aside from within a custom ReauthFunc, this field shouldn't be set by an application.
56	// To safely read or write this value, call `Token` or `SetToken`, respectively
57	TokenID string
58
59	// EndpointLocator describes how this provider discovers the endpoints for
60	// its constituent services.
61	EndpointLocator EndpointLocator
62
63	// HTTPClient allows users to interject arbitrary http, https, or other transit behaviors.
64	HTTPClient http.Client
65
66	// UserAgent represents the User-Agent header in the HTTP request.
67	UserAgent UserAgent
68
69	// ReauthFunc is the function used to re-authenticate the user if the request
70	// fails with a 401 HTTP response code. This a needed because there may be multiple
71	// authentication functions for different Identity service versions.
72	ReauthFunc func() error
73
74	mut *sync.RWMutex
75
76	reauthmut *reauthlock
77}
78
79type reauthlock struct {
80	sync.RWMutex
81	reauthing bool
82}
83
84// AuthenticatedHeaders returns a map of HTTP headers that are common for all
85// authenticated service requests.
86func (client *ProviderClient) AuthenticatedHeaders() (m map[string]string) {
87	if client.reauthmut != nil {
88		client.reauthmut.RLock()
89		if client.reauthmut.reauthing {
90			client.reauthmut.RUnlock()
91			return
92		}
93		client.reauthmut.RUnlock()
94	}
95	t := client.Token()
96	if t == "" {
97		return
98	}
99	return map[string]string{"X-Auth-Token": t}
100}
101
102// UseTokenLock creates a mutex that is used to allow safe concurrent access to the auth token.
103// If the application's ProviderClient is not used concurrently, this doesn't need to be called.
104func (client *ProviderClient) UseTokenLock() {
105	client.mut = new(sync.RWMutex)
106	client.reauthmut = new(reauthlock)
107}
108
109// Token safely reads the value of the auth token from the ProviderClient. Applications should
110// call this method to access the token instead of the TokenID field
111func (client *ProviderClient) Token() string {
112	if client.mut != nil {
113		client.mut.RLock()
114		defer client.mut.RUnlock()
115	}
116	return client.TokenID
117}
118
119// SetToken safely sets the value of the auth token in the ProviderClient. Applications may
120// use this method in a custom ReauthFunc
121func (client *ProviderClient) SetToken(t string) {
122	if client.mut != nil {
123		client.mut.Lock()
124		defer client.mut.Unlock()
125	}
126	client.TokenID = t
127}
128
129//Reauthenticate calls client.ReauthFunc in a thread-safe way. If this is
130//called because of a 401 response, the caller may pass the previous token. In
131//this case, the reauthentication can be skipped if another thread has already
132//reauthenticated in the meantime. If no previous token is known, an empty
133//string should be passed instead to force unconditional reauthentication.
134func (client *ProviderClient) Reauthenticate(previousToken string) (err error) {
135	if client.ReauthFunc == nil {
136		return nil
137	}
138
139	if client.mut == nil {
140		return client.ReauthFunc()
141	}
142	client.mut.Lock()
143	defer client.mut.Unlock()
144
145	client.reauthmut.Lock()
146	client.reauthmut.reauthing = true
147	client.reauthmut.Unlock()
148
149	if previousToken == "" || client.TokenID == previousToken {
150		err = client.ReauthFunc()
151	}
152
153	client.reauthmut.Lock()
154	client.reauthmut.reauthing = false
155	client.reauthmut.Unlock()
156	return
157}
158
159// RequestOpts customizes the behavior of the provider.Request() method.
160type RequestOpts struct {
161	// JSONBody, if provided, will be encoded as JSON and used as the body of the HTTP request. The
162	// content type of the request will default to "application/json" unless overridden by MoreHeaders.
163	// It's an error to specify both a JSONBody and a RawBody.
164	JSONBody interface{}
165	// RawBody contains an io.Reader that will be consumed by the request directly. No content-type
166	// will be set unless one is provided explicitly by MoreHeaders.
167	RawBody io.Reader
168	// JSONResponse, if provided, will be populated with the contents of the response body parsed as
169	// JSON.
170	JSONResponse interface{}
171	// OkCodes contains a list of numeric HTTP status codes that should be interpreted as success. If
172	// the response has a different code, an error will be returned.
173	OkCodes []int
174	// MoreHeaders specifies additional HTTP headers to be provide on the request. If a header is
175	// provided with a blank value (""), that header will be *omitted* instead: use this to suppress
176	// the default Accept header or an inferred Content-Type, for example.
177	MoreHeaders map[string]string
178	// ErrorContext specifies the resource error type to return if an error is encountered.
179	// This lets resources override default error messages based on the response status code.
180	ErrorContext error
181}
182
183var applicationJSON = "application/json"
184
185// Request performs an HTTP request using the ProviderClient's current HTTPClient. An authentication
186// header will automatically be provided.
187func (client *ProviderClient) Request(method, url string, options *RequestOpts) (*http.Response, error) {
188	var body io.Reader
189	var contentType *string
190
191	// Derive the content body by either encoding an arbitrary object as JSON, or by taking a provided
192	// io.ReadSeeker as-is. Default the content-type to application/json.
193	if options.JSONBody != nil {
194		if options.RawBody != nil {
195			panic("Please provide only one of JSONBody or RawBody to gophercloud.Request().")
196		}
197
198		rendered, err := json.Marshal(options.JSONBody)
199		if err != nil {
200			return nil, err
201		}
202
203		body = bytes.NewReader(rendered)
204		contentType = &applicationJSON
205	}
206
207	if options.RawBody != nil {
208		body = options.RawBody
209	}
210
211	// Construct the http.Request.
212	req, err := http.NewRequest(method, url, body)
213	if err != nil {
214		return nil, err
215	}
216
217	// Populate the request headers. Apply options.MoreHeaders last, to give the caller the chance to
218	// modify or omit any header.
219	if contentType != nil {
220		req.Header.Set("Content-Type", *contentType)
221	}
222	req.Header.Set("Accept", applicationJSON)
223
224	// Set the User-Agent header
225	req.Header.Set("User-Agent", client.UserAgent.Join())
226
227	if options.MoreHeaders != nil {
228		for k, v := range options.MoreHeaders {
229			if v != "" {
230				req.Header.Set(k, v)
231			} else {
232				req.Header.Del(k)
233			}
234		}
235	}
236
237	// get latest token from client
238	for k, v := range client.AuthenticatedHeaders() {
239		req.Header.Set(k, v)
240	}
241
242	// Set connection parameter to close the connection immediately when we've got the response
243	req.Close = true
244
245	prereqtok := req.Header.Get("X-Auth-Token")
246
247	// Issue the request.
248	resp, err := client.HTTPClient.Do(req)
249	if err != nil {
250		return nil, err
251	}
252
253	// Allow default OkCodes if none explicitly set
254	if options.OkCodes == nil {
255		options.OkCodes = defaultOkCodes(method)
256	}
257
258	// Validate the HTTP response status.
259	var ok bool
260	for _, code := range options.OkCodes {
261		if resp.StatusCode == code {
262			ok = true
263			break
264		}
265	}
266
267	if !ok {
268		body, _ := ioutil.ReadAll(resp.Body)
269		resp.Body.Close()
270		respErr := ErrUnexpectedResponseCode{
271			URL:      url,
272			Method:   method,
273			Expected: options.OkCodes,
274			Actual:   resp.StatusCode,
275			Body:     body,
276		}
277
278		errType := options.ErrorContext
279		switch resp.StatusCode {
280		case http.StatusBadRequest:
281			err = ErrDefault400{respErr}
282			if error400er, ok := errType.(Err400er); ok {
283				err = error400er.Error400(respErr)
284			}
285		case http.StatusUnauthorized:
286			if client.ReauthFunc != nil {
287				err = client.Reauthenticate(prereqtok)
288				if err != nil {
289					e := &ErrUnableToReauthenticate{}
290					e.ErrOriginal = respErr
291					return nil, e
292				}
293				if options.RawBody != nil {
294					if seeker, ok := options.RawBody.(io.Seeker); ok {
295						seeker.Seek(0, 0)
296					}
297				}
298				// make a new call to request with a nil reauth func in order to avoid infinite loop
299				reauthFunc := client.ReauthFunc
300				client.ReauthFunc = nil
301				resp, err = client.Request(method, url, options)
302				client.ReauthFunc = reauthFunc
303				if err != nil {
304					switch err.(type) {
305					case *ErrUnexpectedResponseCode:
306						e := &ErrErrorAfterReauthentication{}
307						e.ErrOriginal = err.(*ErrUnexpectedResponseCode)
308						return nil, e
309					default:
310						e := &ErrErrorAfterReauthentication{}
311						e.ErrOriginal = err
312						return nil, e
313					}
314				}
315				return resp, nil
316			}
317			err = ErrDefault401{respErr}
318			if error401er, ok := errType.(Err401er); ok {
319				err = error401er.Error401(respErr)
320			}
321		case http.StatusForbidden:
322			err = ErrDefault403{respErr}
323			if error403er, ok := errType.(Err403er); ok {
324				err = error403er.Error403(respErr)
325			}
326		case http.StatusNotFound:
327			err = ErrDefault404{respErr}
328			if error404er, ok := errType.(Err404er); ok {
329				err = error404er.Error404(respErr)
330			}
331		case http.StatusMethodNotAllowed:
332			err = ErrDefault405{respErr}
333			if error405er, ok := errType.(Err405er); ok {
334				err = error405er.Error405(respErr)
335			}
336		case http.StatusRequestTimeout:
337			err = ErrDefault408{respErr}
338			if error408er, ok := errType.(Err408er); ok {
339				err = error408er.Error408(respErr)
340			}
341		case 429:
342			err = ErrDefault429{respErr}
343			if error429er, ok := errType.(Err429er); ok {
344				err = error429er.Error429(respErr)
345			}
346		case http.StatusInternalServerError:
347			err = ErrDefault500{respErr}
348			if error500er, ok := errType.(Err500er); ok {
349				err = error500er.Error500(respErr)
350			}
351		case http.StatusServiceUnavailable:
352			err = ErrDefault503{respErr}
353			if error503er, ok := errType.(Err503er); ok {
354				err = error503er.Error503(respErr)
355			}
356		}
357
358		if err == nil {
359			err = respErr
360		}
361
362		return resp, err
363	}
364
365	// Parse the response body as JSON, if requested to do so.
366	if options.JSONResponse != nil {
367		defer resp.Body.Close()
368		if err := json.NewDecoder(resp.Body).Decode(options.JSONResponse); err != nil {
369			return nil, err
370		}
371	}
372
373	return resp, nil
374}
375
376func defaultOkCodes(method string) []int {
377	switch {
378	case method == "GET":
379		return []int{200}
380	case method == "POST":
381		return []int{201, 202}
382	case method == "PUT":
383		return []int{201, 202}
384	case method == "PATCH":
385		return []int{200, 202, 204}
386	case method == "DELETE":
387		return []int{202, 204}
388	}
389
390	return []int{}
391}
392