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 reauthing bool 98 reauthingErr error 99 done *sync.Cond 100} 101 102// AuthenticatedHeaders returns a map of HTTP headers that are common for all 103// authenticated service requests. Blocks if Reauthenticate is in progress. 104func (client *ProviderClient) AuthenticatedHeaders() (m map[string]string) { 105 if client.IsThrowaway() { 106 return 107 } 108 if client.reauthmut != nil { 109 client.reauthmut.Lock() 110 for client.reauthmut.reauthing { 111 client.reauthmut.done.Wait() 112 } 113 client.reauthmut.Unlock() 114 } 115 t := client.Token() 116 if t == "" { 117 return 118 } 119 return map[string]string{"X-Auth-Token": t} 120} 121 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. 124func (client *ProviderClient) UseTokenLock() { 125 client.mut = new(sync.RWMutex) 126 client.reauthmut = new(reauthlock) 127} 128 129// GetAuthResult returns the result from the request that was used to obtain a 130// provider client's Keystone token. 131// 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. 135func (client *ProviderClient) GetAuthResult() AuthResult { 136 if client.mut != nil { 137 client.mut.RLock() 138 defer client.mut.RUnlock() 139 } 140 return client.authResult 141} 142 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 145func (client *ProviderClient) Token() string { 146 if client.mut != nil { 147 client.mut.RLock() 148 defer client.mut.RUnlock() 149 } 150 return client.TokenID 151} 152 153// SetToken safely sets the value of the auth token in the ProviderClient. Applications may 154// use this method in a custom ReauthFunc. 155// 156// WARNING: This function is deprecated. Use SetTokenAndAuthResult() instead. 157func (client *ProviderClient) SetToken(t string) { 158 if client.mut != nil { 159 client.mut.Lock() 160 defer client.mut.Unlock() 161 } 162 client.TokenID = t 163 client.authResult = nil 164} 165 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. 169func (client *ProviderClient) SetTokenAndAuthResult(r AuthResult) error { 170 tokenID := "" 171 var err error 172 if r != nil { 173 tokenID, err = r.ExtractTokenID() 174 if err != nil { 175 return err 176 } 177 } 178 179 if client.mut != nil { 180 client.mut.Lock() 181 defer client.mut.Unlock() 182 } 183 client.TokenID = tokenID 184 client.authResult = r 185 return nil 186} 187 188// CopyTokenFrom safely copies the token from another ProviderClient into the 189// this one. 190func (client *ProviderClient) CopyTokenFrom(other *ProviderClient) { 191 if client.mut != nil { 192 client.mut.Lock() 193 defer client.mut.Unlock() 194 } 195 if other.mut != nil && other.mut != client.mut { 196 other.mut.RLock() 197 defer other.mut.RUnlock() 198 } 199 client.TokenID = other.TokenID 200 client.authResult = other.authResult 201} 202 203// IsThrowaway safely reads the value of the client Throwaway field. 204func (client *ProviderClient) IsThrowaway() bool { 205 if client.reauthmut != nil { 206 client.reauthmut.RLock() 207 defer client.reauthmut.RUnlock() 208 } 209 return client.Throwaway 210} 211 212// SetThrowaway safely sets the value of the client Throwaway field. 213func (client *ProviderClient) SetThrowaway(v bool) { 214 if client.reauthmut != nil { 215 client.reauthmut.Lock() 216 defer client.reauthmut.Unlock() 217 } 218 client.Throwaway = v 219} 220 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. 226func (client *ProviderClient) Reauthenticate(previousToken string) (err error) { 227 if client.ReauthFunc == nil { 228 return nil 229 } 230 231 if client.reauthmut == nil { 232 return client.ReauthFunc() 233 } 234 235 client.reauthmut.Lock() 236 if client.reauthmut.reauthing { 237 for !client.reauthmut.reauthing { 238 client.reauthmut.done.Wait() 239 } 240 err = client.reauthmut.reauthingErr 241 client.reauthmut.Unlock() 242 return err 243 } 244 client.reauthmut.Unlock() 245 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() 251 252 if previousToken == "" || client.TokenID == previousToken { 253 err = client.ReauthFunc() 254 } 255 256 client.reauthmut.Lock() 257 client.reauthmut.reauthing = false 258 client.reauthmut.reauthingErr = err 259 client.reauthmut.done.Broadcast() 260 client.reauthmut.Unlock() 261 return 262} 263 264// RequestOpts customizes the behavior of the provider.Request() method. 265type 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. 269 JSONBody interface{} 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. 272 RawBody io.Reader 273 // JSONResponse, if provided, will be populated with the contents of the response body parsed as 274 // JSON. 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. 278 OkCodes []int 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. 285 ErrorContext error 286} 287 288var applicationJSON = "application/json" 289 290// Request performs an HTTP request using the ProviderClient's current HTTPClient. An authentication 291// header will automatically be provided. 292func (client *ProviderClient) Request(method, url string, options *RequestOpts) (*http.Response, error) { 293 var body io.Reader 294 var contentType *string 295 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()") 301 } 302 303 rendered, err := json.Marshal(options.JSONBody) 304 if err != nil { 305 return nil, err 306 } 307 308 body = bytes.NewReader(rendered) 309 contentType = &applicationJSON 310 } 311 312 if options.RawBody != nil { 313 body = options.RawBody 314 } 315 316 // Construct the http.Request. 317 req, err := http.NewRequest(method, url, body) 318 if err != nil { 319 return nil, err 320 } 321 if client.Context != nil { 322 req = req.WithContext(client.Context) 323 } 324 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) 329 } 330 req.Header.Set("Accept", applicationJSON) 331 332 // Set the User-Agent header 333 req.Header.Set("User-Agent", client.UserAgent.Join()) 334 335 if options.MoreHeaders != nil { 336 for k, v := range options.MoreHeaders { 337 if v != "" { 338 req.Header.Set(k, v) 339 } else { 340 req.Header.Del(k) 341 } 342 } 343 } 344 345 // get latest token from client 346 for k, v := range client.AuthenticatedHeaders() { 347 req.Header.Set(k, v) 348 } 349 350 // Set connection parameter to close the connection immediately when we've got the response 351 req.Close = true 352 353 prereqtok := req.Header.Get("X-Auth-Token") 354 355 // Issue the request. 356 resp, err := client.HTTPClient.Do(req) 357 if err != nil { 358 return nil, err 359 } 360 361 // Allow default OkCodes if none explicitly set 362 okc := options.OkCodes 363 if okc == nil { 364 okc = defaultOkCodes(method) 365 } 366 367 // Validate the HTTP response status. 368 var ok bool 369 for _, code := range okc { 370 if resp.StatusCode == code { 371 ok = true 372 break 373 } 374 } 375 376 if !ok { 377 body, _ := ioutil.ReadAll(resp.Body) 378 resp.Body.Close() 379 respErr := ErrUnexpectedResponseCode{ 380 URL: url, 381 Method: method, 382 Expected: options.OkCodes, 383 Actual: resp.StatusCode, 384 Body: body, 385 } 386 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) 393 } 394 case http.StatusUnauthorized: 395 if client.ReauthFunc != nil { 396 err = client.Reauthenticate(prereqtok) 397 if err != nil { 398 e := &ErrUnableToReauthenticate{} 399 e.ErrOriginal = respErr 400 return nil, e 401 } 402 if options.RawBody != nil { 403 if seeker, ok := options.RawBody.(io.Seeker); ok { 404 seeker.Seek(0, 0) 405 } 406 } 407 resp, err = client.Request(method, url, options) 408 if err != nil { 409 switch err.(type) { 410 case *ErrUnexpectedResponseCode: 411 e := &ErrErrorAfterReauthentication{} 412 e.ErrOriginal = err.(*ErrUnexpectedResponseCode) 413 return nil, e 414 default: 415 e := &ErrErrorAfterReauthentication{} 416 e.ErrOriginal = err 417 return nil, e 418 } 419 } 420 return resp, nil 421 } 422 err = ErrDefault401{respErr} 423 if error401er, ok := errType.(Err401er); ok { 424 err = error401er.Error401(respErr) 425 } 426 case http.StatusForbidden: 427 err = ErrDefault403{respErr} 428 if error403er, ok := errType.(Err403er); ok { 429 err = error403er.Error403(respErr) 430 } 431 case http.StatusNotFound: 432 err = ErrDefault404{respErr} 433 if error404er, ok := errType.(Err404er); ok { 434 err = error404er.Error404(respErr) 435 } 436 case http.StatusMethodNotAllowed: 437 err = ErrDefault405{respErr} 438 if error405er, ok := errType.(Err405er); ok { 439 err = error405er.Error405(respErr) 440 } 441 case http.StatusRequestTimeout: 442 err = ErrDefault408{respErr} 443 if error408er, ok := errType.(Err408er); ok { 444 err = error408er.Error408(respErr) 445 } 446 case http.StatusConflict: 447 err = ErrDefault409{respErr} 448 if error409er, ok := errType.(Err409er); ok { 449 err = error409er.Error409(respErr) 450 } 451 case 429: 452 err = ErrDefault429{respErr} 453 if error429er, ok := errType.(Err429er); ok { 454 err = error429er.Error429(respErr) 455 } 456 case http.StatusInternalServerError: 457 err = ErrDefault500{respErr} 458 if error500er, ok := errType.(Err500er); ok { 459 err = error500er.Error500(respErr) 460 } 461 case http.StatusServiceUnavailable: 462 err = ErrDefault503{respErr} 463 if error503er, ok := errType.(Err503er); ok { 464 err = error503er.Error503(respErr) 465 } 466 } 467 468 if err == nil { 469 err = respErr 470 } 471 472 return resp, err 473 } 474 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 { 479 return nil, err 480 } 481 } 482 483 return resp, nil 484} 485 486func defaultOkCodes(method string) []int { 487 switch { 488 case method == "GET": 489 return []int{200} 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} 498 } 499 500 return []int{} 501} 502