github.com/huaweicloud/golangsdk@v0.0.0-20210831081626-d823fe11ceba/provider_client.go (about) 1 package golangsdk 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 = "golangsdk/2.0.0" 18 DefaultMaxBackoffRetries = 5 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 RetryFunc func(context.Context, *ErrUnexpectedResponseCode, error, uint) error 29 30 // Prepend prepends a user-defined string to the default User-Agent string. Users 31 // may pass in one or more strings to prepend. 32 func (ua *UserAgent) Prepend(s ...string) { 33 ua.prepend = append(s, ua.prepend...) 34 } 35 36 // Join concatenates all the user-defined User-Agend strings with the default 37 // Gophercloud User-Agent string. 38 func (ua *UserAgent) Join() string { 39 uaSlice := append(ua.prepend, DefaultUserAgent) 40 return strings.Join(uaSlice, " ") 41 } 42 43 // ProviderClient stores details that are required to interact with any 44 // services within a specific provider's API. 45 // 46 // Generally, you acquire a ProviderClient by calling the NewClient method in 47 // the appropriate provider's child package, providing whatever authentication 48 // credentials are required. 49 type ProviderClient struct { 50 // IdentityBase is the base URL used for a particular provider's identity 51 // service - it will be used when issuing authenticatation requests. It 52 // should point to the root resource of the identity service, not a specific 53 // identity version. 54 IdentityBase string 55 56 // IdentityEndpoint is the identity endpoint. This may be a specific version 57 // of the identity service. If this is the case, this endpoint is used rather 58 // than querying versions first. 59 IdentityEndpoint string 60 61 // TokenID is the ID of the most recently issued valid token. 62 // NOTE: Aside from within a custom ReauthFunc, this field shouldn't be set by an application. 63 // To safely read or write this value, call `Token` or `SetToken`, respectively 64 TokenID string 65 66 // ProjectID is the ID of project to which User is authorized. 67 ProjectID string 68 69 // DomainID is the ID of project to which User is authorized. 70 DomainID 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 // AKSKAuthOptions provides the value for AK/SK authentication, it should be nil if you use token authentication, 88 // Otherwise, it must have a value 89 AKSKAuthOptions AKSKAuthOptions 90 91 // Context is the context passed to the HTTP request. 92 Context context.Context 93 94 // Retry backoff func 95 RetryBackoffFunc RetryFunc 96 97 // MaxBackoffRetries set the maximum number of backoffs. When not set, defaults to DefaultMaxBackoffRetries 98 MaxBackoffRetries uint 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 109 // reauthlock represents a set of attributes used to help in the reauthentication process. 110 type reauthlock struct { 111 sync.RWMutex 112 reauthing bool 113 } 114 115 // AuthenticatedHeaders returns a map of HTTP headers that are common for all 116 // authenticated service requests. 117 func (client *ProviderClient) AuthenticatedHeaders() (m map[string]string) { 118 if client.reauthmut != nil { 119 client.reauthmut.RLock() 120 if client.reauthmut.reauthing { 121 client.reauthmut.RUnlock() 122 return 123 } 124 client.reauthmut.RUnlock() 125 } 126 t := client.Token() 127 if t == "" { 128 return 129 } 130 return map[string]string{"X-Auth-Token": t} 131 } 132 133 // UseTokenLock creates a mutex that is used to allow safe concurrent access to the auth token. 134 // If the application's ProviderClient is not used concurrently, this doesn't need to be called. 135 func (client *ProviderClient) UseTokenLock() { 136 client.mut = new(sync.RWMutex) 137 client.reauthmut = new(reauthlock) 138 } 139 140 // Token safely reads the value of the auth token from the ProviderClient. Applications should 141 // call this method to access the token instead of the TokenID field 142 func (client *ProviderClient) Token() string { 143 if client.mut != nil { 144 client.mut.RLock() 145 defer client.mut.RUnlock() 146 } 147 return client.TokenID 148 } 149 150 // SetToken safely sets the value of the auth token in the ProviderClient. Applications may 151 // use this method in a custom ReauthFunc 152 func (client *ProviderClient) SetToken(t string) { 153 if client.mut != nil { 154 client.mut.Lock() 155 defer client.mut.Unlock() 156 } 157 client.TokenID = t 158 } 159 160 // Reauthenticate calls client.ReauthFunc in a thread-safe way. If this is 161 // called because of a 401 response, the caller may pass the previous token. In 162 // this case, the reauthentication can be skipped if another thread has already 163 // reauthenticated in the meantime. If no previous token is known, an empty 164 // string should be passed instead to force unconditional reauthentication. 165 func (client *ProviderClient) Reauthenticate(previousToken string) (err error) { 166 if client.ReauthFunc == nil { 167 return nil 168 } 169 170 if client.mut == nil { 171 return client.ReauthFunc() 172 } 173 174 client.mut.Lock() 175 defer client.mut.Unlock() 176 177 client.reauthmut.Lock() 178 client.reauthmut.reauthing = true 179 client.reauthmut.Unlock() 180 181 if previousToken == "" || client.TokenID == previousToken { 182 err = client.ReauthFunc() 183 } 184 185 client.reauthmut.Lock() 186 client.reauthmut.reauthing = false 187 client.reauthmut.Unlock() 188 return 189 } 190 191 // RequestOpts customizes the behavior of the provider.Request() method. 192 type RequestOpts struct { 193 // JSONBody, if provided, will be encoded as JSON and used as the body of the HTTP request. The 194 // content type of the request will default to "application/json" unless overridden by MoreHeaders. 195 // It's an error to specify both a JSONBody and a RawBody. 196 JSONBody interface{} 197 // RawBody contains an io.Reader that will be consumed by the request directly. No content-type 198 // will be set unless one is provided explicitly by MoreHeaders. 199 RawBody io.Reader 200 // JSONResponse, if provided, will be populated with the contents of the response body parsed as 201 // JSON. 202 JSONResponse interface{} 203 // OkCodes contains a list of numeric HTTP status codes that should be interpreted as success. If 204 // the response has a different code, an error will be returned. 205 OkCodes []int 206 // MoreHeaders specifies additional HTTP headers to be provide on the request. If a header is 207 // provided with a blank value (""), that header will be *omitted* instead: use this to suppress 208 // the default Accept header or an inferred Content-Type, for example. 209 MoreHeaders map[string]string 210 // ErrorContext specifies the resource error type to return if an error is encountered. 211 // This lets resources override default error messages based on the response status code. 212 ErrorContext error 213 // KeepResponseBody specifies whether to keep the HTTP response body. Usually used, when the HTTP 214 // response body is considered for further use. Valid when JSONResponse is nil. 215 KeepResponseBody bool 216 } 217 218 // requestState contains temporary state for a single ProviderClient.Request() call. 219 type requestState struct { 220 // This flag indicates if we have reauthenticated during this request because of a 401 response. 221 // It ensures that we don't reauthenticate multiple times for a single request. If we 222 // reauthenticate, but keep getting 401 responses with the fresh token, reauthenticating some more 223 // will just get us into an infinite loop. 224 hasReauthenticated bool 225 // Retry-After backoff counter, increments during each backoff call 226 retries uint 227 } 228 229 var applicationJSON = "application/json" 230 231 func jsonMarshal(t interface{}) ([]byte, error) { 232 buffer := &bytes.Buffer{} 233 enc := json.NewEncoder(buffer) 234 enc.SetEscapeHTML(false) 235 err := enc.Encode(t) 236 return buffer.Bytes(), err 237 } 238 239 // Request performs an HTTP request using the ProviderClient's current HTTPClient. An authentication 240 // header will automatically be provided. 241 func (client *ProviderClient) Request(method, url string, options *RequestOpts) (*http.Response, error) { 242 return client.doRequest(method, url, options, &requestState{ 243 hasReauthenticated: false, 244 }) 245 } 246 247 func (client *ProviderClient) doRequest(method, url string, options *RequestOpts, state *requestState) (*http.Response, error) { 248 var body io.Reader 249 var contentType *string 250 251 // Derive the content body by either encoding an arbitrary object as JSON, or by taking a provided 252 // io.ReadSeeker as-is. Default the content-type to application/json. 253 if options.JSONBody != nil { 254 if options.RawBody != nil { 255 return nil, errors.New("Please provide only one of JSONBody or RawBody to golangsdk.Request()") 256 } 257 258 rendered, err := jsonMarshal(options.JSONBody) 259 if err != nil { 260 return nil, err 261 } 262 263 body = bytes.NewReader(rendered) 264 contentType = &applicationJSON 265 } 266 267 // Return an error, when "KeepResponseBody" is true and "JSONResponse" is not nil 268 if options.KeepResponseBody && options.JSONResponse != nil { 269 return nil, errors.New("cannot use KeepResponseBody when JSONResponse is not nil") 270 } 271 272 if options.RawBody != nil { 273 body = options.RawBody 274 } 275 276 // Construct the http.Request. 277 req, err := http.NewRequest(method, url, body) 278 if err != nil { 279 return nil, err 280 } 281 if client.Context != nil { 282 req = req.WithContext(client.Context) 283 } 284 285 // Populate the request headers. Apply options.MoreHeaders last, to give the caller the chance to 286 // modify or omit any header. 287 if contentType != nil { 288 req.Header.Set("Content-Type", *contentType) 289 } 290 req.Header.Set("Accept", applicationJSON) 291 292 // Set the User-Agent header 293 req.Header.Set("User-Agent", client.UserAgent.Join()) 294 295 if options.MoreHeaders != nil { 296 for k, v := range options.MoreHeaders { 297 if v != "" { 298 req.Header.Set(k, v) 299 } else { 300 req.Header.Del(k) 301 } 302 } 303 } 304 305 // get latest token from client 306 for k, v := range client.AuthenticatedHeaders() { 307 req.Header.Set(k, v) 308 } 309 310 prereqtok := req.Header.Get("X-Auth-Token") 311 312 if client.AKSKAuthOptions.AccessKey != "" { 313 Sign(req, SignOptions{ 314 AccessKey: client.AKSKAuthOptions.AccessKey, 315 SecretKey: client.AKSKAuthOptions.SecretKey, 316 }) 317 if client.AKSKAuthOptions.ProjectId != "" { 318 req.Header.Set("X-Project-Id", client.AKSKAuthOptions.ProjectId) 319 } 320 if client.AKSKAuthOptions.DomainID != "" { 321 req.Header.Set("X-Domain-Id", client.AKSKAuthOptions.DomainID) 322 } 323 if client.AKSKAuthOptions.SecurityToken != "" { 324 req.Header.Set("X-Security-Token", client.AKSKAuthOptions.SecurityToken) 325 } 326 } 327 328 // Issue the request. 329 resp, err := client.HTTPClient.Do(req) 330 if err != nil { 331 return nil, err 332 } 333 334 // Allow default OkCodes if none explicitly set 335 okc := options.OkCodes 336 if okc == nil { 337 okc = defaultOkCodes(method) 338 } 339 340 // Validate the HTTP response status. 341 var ok bool 342 for _, code := range okc { 343 if resp.StatusCode == code { 344 ok = true 345 break 346 } 347 } 348 349 if !ok { 350 body, _ := ioutil.ReadAll(resp.Body) 351 resp.Body.Close() 352 respErr := ErrUnexpectedResponseCode{ 353 URL: url, 354 Method: method, 355 Expected: okc, 356 Actual: resp.StatusCode, 357 Body: body, 358 } 359 360 errType := options.ErrorContext 361 switch resp.StatusCode { 362 case http.StatusBadRequest: 363 err = ErrDefault400{respErr} 364 if error400er, ok := errType.(Err400er); ok { 365 err = error400er.Error400(respErr) 366 } 367 case http.StatusUnauthorized: 368 if client.ReauthFunc != nil && !state.hasReauthenticated { 369 err = client.Reauthenticate(prereqtok) 370 if err != nil { 371 e := &ErrUnableToReauthenticate{} 372 e.ErrOriginal = respErr 373 return nil, e 374 } 375 if options.RawBody != nil { 376 if seeker, ok := options.RawBody.(io.Seeker); ok { 377 seeker.Seek(0, 0) 378 } 379 } 380 state.hasReauthenticated = true 381 resp, err = client.doRequest(method, url, options, state) 382 if err != nil { 383 switch err.(type) { 384 case *ErrUnexpectedResponseCode: 385 e := &ErrErrorAfterReauthentication{} 386 e.ErrOriginal = err.(*ErrUnexpectedResponseCode) 387 return nil, e 388 default: 389 e := &ErrErrorAfterReauthentication{} 390 e.ErrOriginal = err 391 return nil, e 392 } 393 } 394 return resp, nil 395 } 396 err = ErrDefault401{respErr} 397 if error401er, ok := errType.(Err401er); ok { 398 err = error401er.Error401(respErr) 399 } 400 case http.StatusForbidden: 401 err = ErrDefault403{respErr} 402 if error403er, ok := errType.(Err403er); ok { 403 err = error403er.Error403(respErr) 404 } 405 case http.StatusNotFound: 406 err = ErrDefault404{respErr} 407 if error404er, ok := errType.(Err404er); ok { 408 err = error404er.Error404(respErr) 409 } 410 case http.StatusMethodNotAllowed: 411 err = ErrDefault405{respErr} 412 if error405er, ok := errType.(Err405er); ok { 413 err = error405er.Error405(respErr) 414 } 415 case http.StatusRequestTimeout: 416 err = ErrDefault408{respErr} 417 if error408er, ok := errType.(Err408er); ok { 418 err = error408er.Error408(respErr) 419 } 420 case http.StatusTooManyRequests: 421 err = ErrDefault429{respErr} 422 if error429er, ok := errType.(Err429er); ok { 423 err = error429er.Error429(respErr) 424 } 425 426 maxTries := client.MaxBackoffRetries 427 if maxTries == 0 { 428 maxTries = DefaultMaxBackoffRetries 429 } 430 431 if f := client.RetryBackoffFunc; f != nil && state.retries < maxTries { 432 var e error 433 434 e = f(client.Context, &respErr, err, state.retries) 435 if e != nil { 436 return resp, e 437 } 438 439 state.retries = state.retries + 1 440 return client.doRequest(method, url, options, state) 441 } 442 case http.StatusInternalServerError: 443 err = ErrDefault500{respErr} 444 if error500er, ok := errType.(Err500er); ok { 445 err = error500er.Error500(respErr) 446 } 447 case http.StatusServiceUnavailable: 448 err = ErrDefault503{respErr} 449 if error503er, ok := errType.(Err503er); ok { 450 err = error503er.Error503(respErr) 451 } 452 } 453 454 if err == nil { 455 err = respErr 456 } 457 458 return resp, err 459 } 460 461 // Parse the response body as JSON, if requested to do so. 462 if options.JSONResponse != nil { 463 defer resp.Body.Close() 464 // Don't decode JSON when there is no content 465 if resp.StatusCode == http.StatusNoContent { 466 // read till EOF, otherwise the connection will be closed and cannot be reused 467 _, err = io.Copy(ioutil.Discard, resp.Body) 468 return resp, err 469 } 470 if err := json.NewDecoder(resp.Body).Decode(options.JSONResponse); err != nil { 471 return nil, err 472 } 473 } 474 475 // Close unused body to allow the HTTP connection to be reused 476 if !options.KeepResponseBody && options.JSONResponse == nil { 477 defer resp.Body.Close() 478 // read till EOF, otherwise the connection will be closed and cannot be reused 479 if _, err := io.Copy(ioutil.Discard, resp.Body); err != nil { 480 return nil, err 481 } 482 } 483 484 return resp, nil 485 } 486 487 func defaultOkCodes(method string) []int { 488 switch method { 489 case "GET", "HEAD": 490 return []int{200} 491 case "POST": 492 return []int{201, 202} 493 case "PUT": 494 return []int{201, 202} 495 case "PATCH": 496 return []int{200, 202, 204} 497 case "DELETE": 498 return []int{202, 204} 499 } 500 501 return []int{} 502 }