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