15 // DefaultUserAgent is the default User-Agent string set in the request header.
16 const DefaultUserAgent = "gophercloud/2.0.0"
18 // UserAgent represents a User-Agent header.
19 type 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.
25 // Prepend prepends a user-defined string to the default User-Agent string. Users
26 // may pass in one or more strings to prepend.
27 func (ua *UserAgent) Prepend(s ...string) {
28 ua.prepend = append(s, ua.prepend...)
31 // Join concatenates all the user-defined User-Agend strings with the default
32 // Gophercloud User-Agent string.
33 func (ua *UserAgent) Join() string {
34 uaSlice := append(ua.prepend, DefaultUserAgent)
35 return strings.Join(uaSlice, " ")
38 // ProviderClient stores details that are required to interact with any
39 // services within a specific provider's API.
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.
44 type 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
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
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
61 // EndpointLocator describes how this provider discovers the endpoints for
62 // its constituent services.
63 EndpointLocator EndpointLocator
65 // HTTPClient allows users to interject arbitrary http, https, or other transit behaviors.
66 HTTPClient http.Client
68 // UserAgent represents the User-Agent header in the HTTP request.
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
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.
80 // Context is the context passed to the HTTP request.
81 Context context.Context
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.
87 // reauthmut is a mutex for reauthentication it attempts to ensure that only one reauthentication
88 // attempt happens at one time.
94 // reauthlock represents a set of attributes used to help in the reauthentication process.
95 type reauthlock struct {
102 // AuthenticatedHeaders returns a map of HTTP headers that are common for all
103 // authenticated service requests. Blocks if Reauthenticate is in progress.
104 func (client *ProviderClient) AuthenticatedHeaders() (m map[string]string) {
105 if client.IsThrowaway() {
108 if client.reauthmut != nil {
109 client.reauthmut.Lock()
110 for client.reauthmut.reauthing {
111 client.reauthmut.done.Wait()
113 client.reauthmut.Unlock()
119 return map[string]string{"X-Auth-Token": t}
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.
124 func (client *ProviderClient) UseTokenLock() {
125 client.mut = new(sync.RWMutex)
126 client.reauthmut = new(reauthlock)
129 // GetAuthResult returns the result from the request that was used to obtain a
130 // provider client's Keystone token.
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.
135 func (client *ProviderClient) GetAuthResult() AuthResult {
136 if client.mut != nil {
138 defer client.mut.RUnlock()
140 return client.authResult
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
145 func (client *ProviderClient) Token() string {
146 if client.mut != nil {
148 defer client.mut.RUnlock()
150 return client.TokenID
153 // SetToken safely sets the value of the auth token in the ProviderClient. Applications may
154 // use this method in a custom ReauthFunc.
156 // WARNING: This function is deprecated. Use SetTokenAndAuthResult() instead.
157 func (client *ProviderClient) SetToken(t string) {
158 if client.mut != nil {
160 defer client.mut.Unlock()
163 client.authResult = nil
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.
169 func (client *ProviderClient) SetTokenAndAuthResult(r AuthResult) error {
173 tokenID, err = r.ExtractTokenID()
179 if client.mut != nil {
181 defer client.mut.Unlock()
183 client.TokenID = tokenID
184 client.authResult = r
188 // CopyTokenFrom safely copies the token from another ProviderClient into the
190 func (client *ProviderClient) CopyTokenFrom(other *ProviderClient) {
191 if client.mut != nil {
193 defer client.mut.Unlock()
195 if other.mut != nil && other.mut != client.mut {
197 defer other.mut.RUnlock()
199 client.TokenID = other.TokenID
200 client.authResult = other.authResult
203 // IsThrowaway safely reads the value of the client Throwaway field.
204 func (client *ProviderClient) IsThrowaway() bool {
205 if client.reauthmut != nil {
206 client.reauthmut.RLock()
207 defer client.reauthmut.RUnlock()
209 return client.Throwaway
212 // SetThrowaway safely sets the value of the client Throwaway field.
213 func (client *ProviderClient) SetThrowaway(v bool) {
214 if client.reauthmut != nil {
215 client.reauthmut.Lock()
216 defer client.reauthmut.Unlock()
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.
226 func (client *ProviderClient) Reauthenticate(previousToken string) (err error) {
227 if client.ReauthFunc == nil {
231 if client.reauthmut == nil {
232 return client.ReauthFunc()
235 client.reauthmut.Lock()
236 if client.reauthmut.reauthing {
237 for !client.reauthmut.reauthing {
238 client.reauthmut.done.Wait()
240 err = client.reauthmut.reauthingErr
241 client.reauthmut.Unlock()
244 client.reauthmut.Unlock()
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()
252 if previousToken == "" || client.TokenID == previousToken {
253 err = client.ReauthFunc()
256 client.reauthmut.Lock()
257 client.reauthmut.reauthing = false
258 client.reauthmut.reauthingErr = err
259 client.reauthmut.done.Broadcast()
260 client.reauthmut.Unlock()
264 // RequestOpts customizes the behavior of the provider.Request() method.
265 type 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.
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.
273 // JSONResponse, if provided, will be populated with the contents of the response body parsed as
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.
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.
288 var applicationJSON = "application/json"
290 // Request performs an HTTP request using the ProviderClient's current HTTPClient. An authentication
291 // header will automatically be provided.
292 func (client *ProviderClient) Request(method, url string, options *RequestOpts) (*http.Response, error) {
294 var contentType *string
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()")
303 rendered, err := json.Marshal(options.JSONBody)
308 body = bytes.NewReader(rendered)
309 contentType = &applicationJSON
312 if options.RawBody != nil {
313 body = options.RawBody
316 // Construct the http.Request.
317 req, err := http.NewRequest(method, url, body)
321 if client.Context != nil {
322 req = req.WithContext(client.Context)
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)
330 req.Header.Set("Accept", applicationJSON)
332 // Set the User-Agent header
333 req.Header.Set("User-Agent", client.UserAgent.Join())
335 if options.MoreHeaders != nil {
336 for k, v := range options.MoreHeaders {
345 // get latest token from client
346 for k, v := range client.AuthenticatedHeaders() {
350 // Set connection parameter to close the connection immediately when we've got the response
353 prereqtok := req.Header.Get("X-Auth-Token")
355 // Issue the request.
356 resp, err := client.HTTPClient.Do(req)
361 // Allow default OkCodes if none explicitly set
362 okc := options.OkCodes
364 okc = defaultOkCodes(method)
367 // Validate the HTTP response status.
369 for _, code := range okc {
370 if resp.StatusCode == code {
377 body, _ := ioutil.ReadAll(resp.Body)
379 respErr := ErrUnexpectedResponseCode{
382 Expected: options.OkCodes,
383 Actual: resp.StatusCode,
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)
394 case http.StatusUnauthorized:
395 if client.ReauthFunc != nil {
396 err = client.Reauthenticate(prereqtok)
398 e := &ErrUnableToReauthenticate{}
399 e.ErrOriginal = respErr
402 if options.RawBody != nil {
403 if seeker, ok := options.RawBody.(io.Seeker); ok {
407 resp, err = client.Request(method, url, options)
410 case *ErrUnexpectedResponseCode:
411 e := &ErrErrorAfterReauthentication{}
412 e.ErrOriginal = err.(*ErrUnexpectedResponseCode)
415 e := &ErrErrorAfterReauthentication{}
422 err = ErrDefault401{respErr}
423 if error401er, ok := errType.(Err401er); ok {
424 err = error401er.Error401(respErr)
426 case http.StatusForbidden:
427 err = ErrDefault403{respErr}
428 if error403er, ok := errType.(Err403er); ok {
429 err = error403er.Error403(respErr)
431 case http.StatusNotFound:
432 err = ErrDefault404{respErr}
433 if error404er, ok := errType.(Err404er); ok {
434 err = error404er.Error404(respErr)
436 case http.StatusMethodNotAllowed:
437 err = ErrDefault405{respErr}
438 if error405er, ok := errType.(Err405er); ok {
439 err = error405er.Error405(respErr)
441 case http.StatusRequestTimeout:
442 err = ErrDefault408{respErr}
443 if error408er, ok := errType.(Err408er); ok {
444 err = error408er.Error408(respErr)
446 case http.StatusConflict:
447 err = ErrDefault409{respErr}
448 if error409er, ok := errType.(Err409er); ok {
449 err = error409er.Error409(respErr)
452 err = ErrDefault429{respErr}
453 if error429er, ok := errType.(Err429er); ok {
454 err = error429er.Error429(respErr)
456 case http.StatusInternalServerError:
457 err = ErrDefault500{respErr}
458 if error500er, ok := errType.(Err500er); ok {
459 err = error500er.Error500(respErr)
461 case http.StatusServiceUnavailable:
462 err = ErrDefault503{respErr}
463 if error503er, ok := errType.(Err503er); ok {
464 err = error503er.Error503(respErr)
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 {
486 func defaultOkCodes(method string) []int {
488 case method == "GET":
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}