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