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