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