client.go raw
1 // Copyright (c) 2016, 2018, 2025, Oracle and/or its affiliates. All rights reserved.
2 // This software is dual-licensed to you under the Universal Permissive License (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl or Apache License 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose either license.
3
4 // Package common provides supporting functions and structs used by service packages
5 package common
6
7 import (
8 "bytes"
9 "context"
10 "fmt"
11 "io"
12 "io/ioutil"
13 "math/rand"
14 "net/http"
15 "net/http/httputil"
16 "net/url"
17 "os"
18 "os/user"
19 "path"
20 "path/filepath"
21 "reflect"
22 "regexp"
23 "runtime"
24 "strconv"
25 "strings"
26 "sync"
27 "sync/atomic"
28 "time"
29 )
30
31 const (
32 // DefaultHostURLTemplate The default url template for service hosts
33 DefaultHostURLTemplate = "%s.%s.oraclecloud.com"
34
35 // requestHeaderAccept The key for passing a header to indicate Accept
36 requestHeaderAccept = "Accept"
37
38 // requestHeaderAuthorization The key for passing a header to indicate Authorization
39 requestHeaderAuthorization = "Authorization"
40
41 // requestHeaderContentLength The key for passing a header to indicate Content Length
42 requestHeaderContentLength = "Content-Length"
43
44 // requestHeaderContentType The key for passing a header to indicate Content Type
45 requestHeaderContentType = "Content-Type"
46
47 // requestHeaderExpect The key for passing a header to indicate Expect/100-Continue
48 requestHeaderExpect = "Expect"
49
50 // requestHeaderDate The key for passing a header to indicate Date
51 requestHeaderDate = "Date"
52
53 // requestHeaderIfMatch The key for passing a header to indicate If Match
54 requestHeaderIfMatch = "if-match"
55
56 // requestHeaderOpcClientInfo The key for passing a header to indicate OPC Client Info
57 requestHeaderOpcClientInfo = "opc-client-info"
58
59 // requestHeaderOpcRetryToken The key for passing a header to indicate OPC Retry Token
60 requestHeaderOpcRetryToken = "opc-retry-token"
61
62 // requestHeaderOpcRequestID The key for unique Oracle-assigned identifier for the request.
63 requestHeaderOpcRequestID = "opc-request-id"
64
65 // requestHeaderOpcClientRequestID The key for unique Oracle-assigned identifier for the request.
66 requestHeaderOpcClientRequestID = "opc-client-request-id"
67
68 // requestHeaderUserAgent The key for passing a header to indicate User Agent
69 requestHeaderUserAgent = "User-Agent"
70
71 // requestHeaderXContentSHA256 The key for passing a header to indicate SHA256 hash
72 requestHeaderXContentSHA256 = "X-Content-SHA256"
73
74 // requestHeaderOpcOboToken The key for passing a header to use obo token
75 requestHeaderOpcOboToken = "opc-obo-token"
76
77 // private constants
78 defaultScheme = "https"
79 defaultSDKMarker = "Oracle-GoSDK"
80 defaultUserAgentTemplate = "%s/%s (%s/%s; go/%s)" //SDK/SDKVersion (OS/OSVersion; Lang/LangVersion)
81 // http.Client.Timeout includes Dial, TLSHandshake, Request, Response header and body
82 defaultTimeout = 60 * time.Second
83 defaultConfigFileName = "config"
84 defaultConfigDirName = ".oci"
85 configFilePathEnvVarName = "OCI_CONFIG_FILE"
86
87 secondaryConfigDirName = ".oraclebmc"
88 maxBodyLenForDebug = 1024 * 1000
89
90 // appendUserAgentEnv The key for retrieving append user agent value from env var
91 appendUserAgentEnv = "OCI_SDK_APPEND_USER_AGENT"
92
93 // requestHeaderOpcClientRetries The key for passing a header to set client retries info
94 requestHeaderOpcClientRetries = "opc-client-retries"
95
96 // isDefaultRetryEnabled The key for set default retry disabled from env var
97 isDefaultRetryEnabled = "OCI_SDK_DEFAULT_RETRY_ENABLED"
98
99 // isDefaultCircuitBreakerEnabled is the key for set default circuit breaker disabled from env var
100 isDefaultCircuitBreakerEnabled = "OCI_SDK_DEFAULT_CIRCUITBREAKER_ENABLED"
101
102 //circuitBreakerNumberOfHistoryResponseEnv is the number of recorded history responses
103 circuitBreakerNumberOfHistoryResponseEnv = "OCI_SDK_CIRCUITBREAKER_NUM_HISTORY_RESPONSE"
104
105 // ociDefaultRefreshIntervalForCustomCerts is the env var for overriding the defaultRefreshIntervalForCustomCerts.
106 // The value represents the refresh interval in minutes and has a higher precedence than defaultRefreshIntervalForCustomCerts
107 // but has a lower precedence then the refresh interval configured via OciGlobalRefreshIntervalForCustomCerts
108 // If the value is negative, then it is assumed that this property is not configured
109 // if the value is Zero, then the refresh of custom certs will be disabled
110 ociDefaultRefreshIntervalForCustomCerts = "OCI_DEFAULT_REFRESH_INTERVAL_FOR_CUSTOM_CERTS"
111
112 // ociDefaultCertsPath is the env var for the path to the SSL cert file
113 ociDefaultCertsPath = "OCI_DEFAULT_CERTS_PATH"
114
115 // ociDefaultClientCertsPath is the env var for the path to the custom client cert
116 ociDefaultClientCertsPath = "OCI_DEFAULT_CLIENT_CERTS_PATH"
117
118 // ociDefaultClientCertsPrivateKeyPath is the env var for the path to the custom client cert private key
119 ociDefaultClientCertsPrivateKeyPath = "OCI_DEFAULT_CLIENT_CERTS_PRIVATE_KEY_PATH"
120
121 //maxAttemptsForRefreshableRetry is the number of retry when 401 happened on a refreshable auth type
122 maxAttemptsForRefreshableRetry = 3
123
124 //defaultRefreshIntervalForCustomCerts is the default refresh interval in minutes
125 defaultRefreshIntervalForCustomCerts = 30
126
127 // CustomClientTimeoutEnvVar allows the user to set the timeout in seconds to be used by each service client.
128 CustomClientTimeoutEnvVar = "OCI_CUSTOM_CLIENT_TIMEOUT"
129
130 // Environment variable to check whether dual stack endpoints should be enabled
131 ociDualStackEndpointEnabledEnvVar = "OCI_DUAL_STACK_ENDPOINT_ENABLED"
132
133 // String representing a single "phrase" of an endpoint template option
134 endpointTemplateOptionPhrase = "((\\w|\\.|\\-)+)"
135
136 // Checks for template for endpoint options
137 patternForEndpointTemplateOptions = "\\{" + endpointTemplateOptionPhrase + "\\?((" + endpointTemplateOptionPhrase + ":" + endpointTemplateOptionPhrase + ")" +
138 "|(" + endpointTemplateOptionPhrase + ":\\s*)|(\\s*:" + endpointTemplateOptionPhrase + "))}"
139
140 dualStackOption = "{dualStack"
141 )
142
143 // OciGlobalRefreshIntervalForCustomCerts is the global policy for overriding the refresh interval in minutes.
144 // This variable has a higher precedence than the env variable OCI_DEFAULT_REFRESH_INTERVAL_FOR_CUSTOM_CERTS
145 // and the defaultRefreshIntervalForCustomCerts values.
146 // If the value is negative, then it is assumed that this property is not configured
147 // if the value is Zero, then the refresh of custom certs will be disabled
148 var OciGlobalRefreshIntervalForCustomCerts int = -1
149
150 // RequestInterceptor function used to customize the request before calling the underlying service
151 type RequestInterceptor func(*http.Request) error
152
153 // HTTPRequestDispatcher wraps the execution of a http request, it is generally implemented by
154 // http.Client.Do, but can be customized for testing
155 type HTTPRequestDispatcher interface {
156 Do(req *http.Request) (*http.Response, error)
157 }
158
159 // CustomClientConfiguration contains configurations set at client level
160 type CustomClientConfiguration struct {
161
162 // Retry policy used on calls made by the client
163 RetryPolicy *RetryPolicy
164
165 // The Circuit Breaker used to regulate calls made by the client
166 CircuitBreaker *OciCircuitBreaker
167
168 // Allows user to decide if they want to use realm specific endpoints
169 RealmSpecificServiceEndpointTemplateEnabled *bool
170
171 // Allows user to decide if they want to use dual stack endpoints
172 EnableDualStackEndpoints *bool
173
174 // Set on creation of the client, based on the below flag from the service spec
175 // x-obmcs-endpoint-template-options: dualStack: true/false
176 ServiceUsesDualStackByDefault *bool
177 }
178
179 // BaseClient struct implements all basic operations to call oci web services.
180 type BaseClient struct {
181 //HTTPClient performs the http network operations
182 HTTPClient HTTPRequestDispatcher
183
184 //Signer performs auth operation
185 Signer HTTPRequestSigner
186
187 //A request interceptor can be used to customize the request before signing and dispatching
188 Interceptor RequestInterceptor
189
190 //The host of the service
191 Host string
192
193 //The user agent
194 UserAgent string
195
196 //Base path for all operations of this client
197 BasePath string
198
199 Configuration CustomClientConfiguration
200 }
201
202 // SetCustomClientConfiguration sets client with retry and other custom configurations
203 func (client *BaseClient) SetCustomClientConfiguration(config CustomClientConfiguration) {
204 client.Configuration = config
205 }
206
207 // RetryPolicy returns the retryPolicy configured for client
208 func (client *BaseClient) RetryPolicy() *RetryPolicy {
209 return client.Configuration.RetryPolicy
210 }
211
212 // Endpoint returns the endpoint configured for client
213 func (client *BaseClient) Endpoint() string {
214 host := client.Host
215 if !strings.Contains(host, "http") &&
216 !strings.Contains(host, "https") {
217 host = fmt.Sprintf("%s://%s", defaultScheme, host)
218 }
219 return host
220 }
221
222 func UpdateEndpointTemplateForOptions(client *BaseClient) {
223 templateRegex := regexp.MustCompile(patternForEndpointTemplateOptions)
224 templates := templateRegex.FindAllString(client.Host, -1)
225 for _, option := range templates {
226 optionParam := ""
227 optionEnabledParam := option[strings.Index(option, "?")+1 : strings.Index(option, ":")]
228 optionDisabledParam := option[strings.Index(option, ":")+1 : strings.Index(option, "}")]
229
230 // Option case: Dual Stack Endpoints
231 if strings.Contains(option, dualStackOption) {
232 dualStackEnvVarValue := os.Getenv(ociDualStackEndpointEnabledEnvVar)
233 if client.IsServiceDualStackEnabledByDefault() {
234 if !client.IsDualStackEndpointEnabled() || (dualStackEnvVarValue != "" && strings.ToLower(dualStackEnvVarValue) == "false") {
235 optionParam = optionDisabledParam
236 } else {
237 optionParam = optionEnabledParam
238 }
239 } else {
240 if client.IsDualStackEndpointEnabled() || (dualStackEnvVarValue != "" && strings.ToLower(dualStackEnvVarValue) == "true") {
241 optionParam = optionEnabledParam
242 } else {
243 optionParam = optionDisabledParam
244 }
245 }
246 }
247 client.Host = strings.Replace(client.Host, option, optionParam, -1)
248 }
249 }
250
251 // UseDualStackEndpointsByDefault sets whether dual stack endpoints are used by default
252 func (client *BaseClient) UseDualStackEndpointsByDefault(useByDefault bool) {
253 client.Configuration.EnableDualStackEndpoints = &useByDefault
254 client.Configuration.ServiceUsesDualStackByDefault = &useByDefault
255 }
256
257 // EnableDualStackEndpoints sets whether dual stack endpoints should be used for this client
258 func (client *BaseClient) EnableDualStackEndpoints(EnableDualStack bool) {
259 client.Configuration.EnableDualStackEndpoints = &EnableDualStack
260 }
261
262 // IsDualStackEndpointEnabled is used to check if Dual Stack Endpoints are Enabled
263 func (client *BaseClient) IsDualStackEndpointEnabled() bool {
264 return client.Configuration.EnableDualStackEndpoints != nil && *client.Configuration.EnableDualStackEndpoints
265 }
266
267 // IsServiceDualStackEnabledByDefault is used to check if Dual Stack Endpoints enabled by default for the service of the client
268 func (client *BaseClient) IsServiceDualStackEnabledByDefault() bool {
269 return client.Configuration.ServiceUsesDualStackByDefault != nil && *client.Configuration.ServiceUsesDualStackByDefault
270 }
271
272 func defaultUserAgent() string {
273 userAgent := fmt.Sprintf(defaultUserAgentTemplate, defaultSDKMarker, Version(), runtime.GOOS, runtime.GOARCH, runtime.Version())
274 appendUA := os.Getenv(appendUserAgentEnv)
275 if appendUA != "" {
276 userAgent = fmt.Sprintf("%s %s", userAgent, appendUA)
277 }
278 return userAgent
279 }
280
281 var clientCounter int64
282
283 func getNextSeed() int64 {
284 newCounterValue := atomic.AddInt64(&clientCounter, 1)
285 return newCounterValue + time.Now().UnixNano()
286 }
287
288 func newBaseClient(signer HTTPRequestSigner, dispatcher HTTPRequestDispatcher) BaseClient {
289 rand.Seed(getNextSeed())
290
291 baseClient := BaseClient{
292 UserAgent: defaultUserAgent(),
293 Interceptor: nil,
294 Signer: signer,
295 HTTPClient: dispatcher,
296 }
297
298 // check the default retry environment variable setting
299 if IsEnvVarTrue(isDefaultRetryEnabled) {
300 defaultRetry := DefaultRetryPolicy()
301 baseClient.Configuration.RetryPolicy = &defaultRetry
302 } else if IsEnvVarFalse(isDefaultRetryEnabled) {
303 policy := NoRetryPolicy()
304 baseClient.Configuration.RetryPolicy = &policy
305 }
306 // check if user defined global retry is configured
307 if GlobalRetry != nil {
308 baseClient.Configuration.RetryPolicy = GlobalRetry
309 }
310
311 baseClient.UseDualStackEndpointsByDefault(false)
312
313 return baseClient
314 }
315
316 func defaultHTTPDispatcher() http.Client {
317 var httpClient http.Client
318 refreshInterval := getCustomCertRefreshInterval()
319 if refreshInterval <= 0 {
320 Debug("Custom cert refresh has been disabled")
321 }
322 var tp = &OciHTTPTransportWrapper{
323 RefreshRate: time.Duration(refreshInterval) * time.Minute,
324 TLSConfigProvider: GetTLSConfigTemplateForTransport(),
325 }
326
327 // Set client timeout to default or value set in environment variable
328 clientTimeout := defaultTimeout
329 if customTimeout := os.Getenv(CustomClientTimeoutEnvVar); customTimeout != "" {
330 if timeInSeconds, err := strconv.Atoi(customTimeout); err != nil || timeInSeconds < 0 {
331 Logf("WARNING: %s set but could not be converted to a postive integer", CustomClientTimeoutEnvVar)
332 } else {
333 Debugf("Using custom client timeout of %s seconds", customTimeout)
334 clientTimeout = time.Duration(timeInSeconds) * time.Second
335 }
336 }
337
338 // Create the underlying HTTP client
339 httpClient = http.Client{
340 Timeout: clientTimeout,
341 Transport: tp,
342 }
343 return httpClient
344 }
345
346 func defaultBaseClient(provider KeyProvider) BaseClient {
347 dispatcher := defaultHTTPDispatcher()
348 signer := DefaultRequestSigner(provider)
349 return newBaseClient(signer, &dispatcher)
350 }
351
352 // DefaultBaseClientWithSigner creates a default base client with a given signer
353 func DefaultBaseClientWithSigner(signer HTTPRequestSigner) BaseClient {
354 dispatcher := defaultHTTPDispatcher()
355 return newBaseClient(signer, &dispatcher)
356 }
357
358 // NewClientWithConfig Create a new client with a configuration provider, the configuration provider
359 // will be used for the default signer as well as reading the region
360 // This function does not check for valid regions to implement forward compatibility
361 func NewClientWithConfig(configProvider ConfigurationProvider) (client BaseClient, err error) {
362 var ok bool
363 if ok, err = IsConfigurationProviderValid(configProvider); !ok {
364 err = fmt.Errorf("can not create client, bad configuration: %s", err.Error())
365 return
366 }
367
368 client = defaultBaseClient(configProvider)
369
370 if authConfig, e := configProvider.AuthType(); e == nil && authConfig.OboToken != nil {
371 Debugf("authConfig's authType is %s, and token content is %s", authConfig.AuthType, *authConfig.OboToken)
372 signOboToken(&client, *authConfig.OboToken, configProvider)
373 }
374
375 return
376 }
377
378 // NewClientWithOboToken Create a new client that will use oboToken for auth
379 func NewClientWithOboToken(configProvider ConfigurationProvider, oboToken string) (client BaseClient, err error) {
380 client, err = NewClientWithConfig(configProvider)
381 if err != nil {
382 return
383 }
384
385 signOboToken(&client, oboToken, configProvider)
386
387 return
388 }
389
390 // Add obo token header to Interceptor and sign to client
391 func signOboToken(client *BaseClient, oboToken string, configProvider ConfigurationProvider) {
392 // Interceptor to add obo token header
393 client.Interceptor = func(request *http.Request) error {
394 request.Header.Add(requestHeaderOpcOboToken, oboToken)
395 return nil
396 }
397 // Obo token will also be signed
398 defaultHeaders := append(DefaultGenericHeaders(), requestHeaderOpcOboToken)
399 client.Signer = RequestSigner(configProvider, defaultHeaders, DefaultBodyHeaders())
400 }
401
402 func getHomeFolder() string {
403 current, e := user.Current()
404 if e != nil {
405 //Give up and try to return something sensible
406 home := os.Getenv("HOME")
407 if home == "" {
408 home = os.Getenv("USERPROFILE")
409 }
410 return home
411 }
412 return current.HomeDir
413 }
414
415 // DefaultConfigProvider returns the default config provider. The default config provider
416 // will look for configurations in 3 places: file in $HOME/.oci/config, HOME/.obmcs/config and
417 // variables names starting with the string TF_VAR. If the same configuration is found in multiple
418 // places the provider will prefer the first one.
419 // If the config file is not placed in the default location, the environment variable
420 // OCI_CONFIG_FILE can provide the config file location.
421 func DefaultConfigProvider() ConfigurationProvider {
422 defaultConfigFile := getDefaultConfigFilePath()
423 homeFolder := getHomeFolder()
424 secondaryConfigFile := filepath.Join(homeFolder, secondaryConfigDirName, defaultConfigFileName)
425
426 defaultFileProvider, _ := ConfigurationProviderFromFile(defaultConfigFile, "")
427 secondaryFileProvider, _ := ConfigurationProviderFromFile(secondaryConfigFile, "")
428 environmentProvider := environmentConfigurationProvider{EnvironmentVariablePrefix: "TF_VAR"}
429
430 provider, _ := ComposingConfigurationProvider([]ConfigurationProvider{defaultFileProvider, secondaryFileProvider, environmentProvider})
431 Debugf("Configuration provided by: %s", provider)
432 return provider
433 }
434
435 // CustomProfileSessionTokenConfigProvider returns the session token config provider of the given profile.
436 // This will look for the configuration in the given config file path.
437 func CustomProfileSessionTokenConfigProvider(customConfigPath string, profile string) ConfigurationProvider {
438 if customConfigPath == "" {
439 customConfigPath = getDefaultConfigFilePath()
440 }
441
442 sessionTokenConfigurationProvider, _ := ConfigurationProviderForSessionTokenWithProfile(customConfigPath, profile, "")
443 Debugf("Configuration provided by: %s", sessionTokenConfigurationProvider)
444 return sessionTokenConfigurationProvider
445 }
446
447 func getDefaultConfigFilePath() string {
448 homeFolder := getHomeFolder()
449 defaultConfigFile := filepath.Join(homeFolder, defaultConfigDirName, defaultConfigFileName)
450 if _, err := os.Stat(defaultConfigFile); err == nil {
451 return defaultConfigFile
452 }
453 Debugf("The %s does not exist, will check env var %s for file path.", defaultConfigFile, configFilePathEnvVarName)
454 // Read configuration file path from OCI_CONFIG_FILE env var
455 fallbackConfigFile, existed := os.LookupEnv(configFilePathEnvVarName)
456 if !existed {
457 Debugf("The env var %s does not exist...", configFilePathEnvVarName)
458 return defaultConfigFile
459 }
460 if _, err := os.Stat(fallbackConfigFile); os.IsNotExist(err) {
461 Debugf("The specified cfg file path in the env var %s does not exist: %s", configFilePathEnvVarName, fallbackConfigFile)
462 return defaultConfigFile
463 }
464 return fallbackConfigFile
465 }
466
467 // setRawPath sets the Path and RawPath fields of the URL based on the provided
468 // escaped path p. It maintains the invariant that RawPath is only specified
469 // when it differs from the default encoding of the path.
470 // For example:
471 // - setPath("/foo/bar") will set Path="/foo/bar" and RawPath=""
472 // - setPath("/foo%2fbar") will set Path="/foo/bar" and RawPath="/foo%2fbar"
473 func setRawPath(u *url.URL) error {
474 oldPath := u.Path
475 path, err := url.PathUnescape(u.Path)
476 if err != nil {
477 return err
478 }
479 u.Path = path
480 if escp := u.EscapedPath(); oldPath == escp {
481 // Default encoding is fine.
482 u.RawPath = ""
483 } else {
484 u.RawPath = oldPath
485 }
486 return nil
487 }
488
489 // CustomProfileConfigProvider returns the config provider of given profile. The custom profile config provider
490 // will look for configurations in 2 places: file in $HOME/.oci/config, and variables names starting with the
491 // string TF_VAR. If the same configuration is found in multiple places the provider will prefer the first one.
492 func CustomProfileConfigProvider(customConfigPath string, profile string) ConfigurationProvider {
493 homeFolder := getHomeFolder()
494 if customConfigPath == "" {
495 customConfigPath = filepath.Join(homeFolder, defaultConfigDirName, defaultConfigFileName)
496 }
497 customFileProvider, _ := ConfigurationProviderFromFileWithProfile(customConfigPath, profile, "")
498 defaultFileProvider, _ := ConfigurationProviderFromFileWithProfile(customConfigPath, "DEFAULT", "")
499 environmentProvider := environmentConfigurationProvider{EnvironmentVariablePrefix: "TF_VAR"}
500 provider, _ := ComposingConfigurationProvider([]ConfigurationProvider{customFileProvider, defaultFileProvider, environmentProvider})
501 Debugf("Configuration provided by: %s", provider)
502 return provider
503 }
504
505 func (client *BaseClient) prepareRequest(request *http.Request) (err error) {
506 if client.UserAgent == "" {
507 return fmt.Errorf("user agent can not be blank")
508 }
509
510 if request.Header == nil {
511 request.Header = http.Header{}
512 }
513 request.Header.Set(requestHeaderUserAgent, client.UserAgent)
514 request.Header.Set(requestHeaderDate, time.Now().UTC().Format(http.TimeFormat))
515
516 if !strings.Contains(client.Host, "http") &&
517 !strings.Contains(client.Host, "https") {
518 client.Host = fmt.Sprintf("%s://%s", defaultScheme, client.Host)
519 }
520
521 clientURL, err := url.Parse(client.Host)
522 if err != nil {
523 return fmt.Errorf("host is invalid. %s", err.Error())
524 }
525 request.URL.Host = clientURL.Host
526 request.URL.Scheme = clientURL.Scheme
527 currentPath := request.URL.Path
528 if !strings.Contains(currentPath, fmt.Sprintf("/%s", client.BasePath)) {
529 request.URL.Path = path.Clean(fmt.Sprintf("/%s/%s", client.BasePath, currentPath))
530 err := setRawPath(request.URL)
531 if err != nil {
532 return err
533 }
534 }
535 return
536 }
537
538 func (client BaseClient) intercept(request *http.Request) (err error) {
539 if client.Interceptor != nil {
540 err = client.Interceptor(request)
541 }
542 return
543 }
544
545 // checkForSuccessfulResponse checks if the response is successful
546 // If Error Code is 4XX/5XX and debug level is set to info, will log the request and response
547 func checkForSuccessfulResponse(res *http.Response, requestBody *io.ReadCloser) error {
548 familyStatusCode := res.StatusCode / 100
549 if familyStatusCode == 4 || familyStatusCode == 5 {
550 IfInfo(func() {
551 // If debug level is set to verbose, the request and request body will be dumped and logged under debug level, this is to avoid duplicate logging
552 if defaultLogger.LogLevel() < verboseLogging {
553 logRequest(res.Request, Logf, noLogging)
554 if requestBody != nil && *requestBody != http.NoBody {
555 bodyContent, _ := ioutil.ReadAll(*requestBody)
556 Logf("Dump Request Body: \n%s", string(bodyContent))
557 }
558 }
559 logResponse(res, Logf, infoLogging)
560 })
561 return newServiceFailureFromResponse(res)
562 }
563 IfDebug(func() {
564 logResponse(res, Debugf, verboseLogging)
565 })
566 return nil
567 }
568
569 func logRequest(request *http.Request, fn func(format string, v ...interface{}), bodyLoggingLevel int) {
570 if request == nil {
571 return
572 }
573 dumpBody := true
574 if checkBodyLengthExceedLimit(request.ContentLength) {
575 fn("not dumping body too big\n")
576 dumpBody = false
577 }
578
579 dumpBody = dumpBody && defaultLogger.LogLevel() >= bodyLoggingLevel && bodyLoggingLevel != noLogging
580 if dump, e := httputil.DumpRequestOut(request, dumpBody); e == nil {
581 fn("Dump Request %s", string(dump))
582 } else {
583 fn("%v\n", e)
584 }
585 }
586
587 func logResponse(response *http.Response, fn func(format string, v ...interface{}), bodyLoggingLevel int) {
588 if response == nil {
589 return
590 }
591 dumpBody := true
592 if checkBodyLengthExceedLimit(response.ContentLength) {
593 fn("not dumping body too big\n")
594 dumpBody = false
595 }
596 dumpBody = dumpBody && defaultLogger.LogLevel() >= bodyLoggingLevel && bodyLoggingLevel != noLogging
597 if dump, e := httputil.DumpResponse(response, dumpBody); e == nil {
598 fn("Dump Response %s", string(dump))
599 } else {
600 fn("%v\n", e)
601 }
602 }
603
604 func checkBodyLengthExceedLimit(contentLength int64) bool {
605 return contentLength > maxBodyLenForDebug
606 }
607
608 // OCIRequest is any request made to an OCI service.
609 type OCIRequest interface {
610 // HTTPRequest assembles an HTTP request.
611 HTTPRequest(method, path string, binaryRequestBody *OCIReadSeekCloser, extraHeaders map[string]string) (http.Request, error)
612 }
613
614 // RequestMetadata is metadata about an OCIRequest. This structure represents the behavior exhibited by the SDK when
615 // issuing (or reissuing) a request.
616 type RequestMetadata struct {
617 // RetryPolicy is the policy for reissuing the request. If no retry policy is set on the request,
618 // then the request will be issued exactly once.
619 RetryPolicy *RetryPolicy
620 }
621
622 // OCIReadSeekCloser is a thread-safe io.ReadSeekCloser to prevent racing with retrying binary requests
623 type OCIReadSeekCloser struct {
624 rc io.ReadCloser
625 lock sync.Mutex
626 isClosed bool
627 }
628
629 // NewOCIReadSeekCloser constructs OCIReadSeekCloser, the only input is binary request body
630 func NewOCIReadSeekCloser(rc io.ReadCloser) *OCIReadSeekCloser {
631 rsc := OCIReadSeekCloser{}
632 rsc.rc = rc
633 return &rsc
634 }
635
636 // Seek is a thread-safe operation, it implements io.seek() interface, if the original request body implements io.seek()
637 // interface, or implements "well-known" data type like os.File, io.SectionReader, or wrapped by ioutil.NopCloser can be supported
638 func (rsc *OCIReadSeekCloser) Seek(offset int64, whence int) (int64, error) {
639 rsc.lock.Lock()
640 defer rsc.lock.Unlock()
641
642 if _, ok := rsc.rc.(io.Seeker); ok {
643 return rsc.rc.(io.Seeker).Seek(offset, whence)
644 }
645 // once the binary request body is wrapped with ioutil.NopCloser:
646 if isNopCloser(rsc.rc) {
647 unwrappedInterface := reflect.ValueOf(rsc.rc).Field(0).Interface()
648 if _, ok := unwrappedInterface.(io.Seeker); ok {
649 return unwrappedInterface.(io.Seeker).Seek(offset, whence)
650 }
651 }
652 return 0, fmt.Errorf("current binary request body type is not seekable, if want to use retry feature, please make sure the request body implements seek() method")
653 }
654
655 // Close is a thread-safe operation, it closes the instance of the OCIReadSeekCloser's access to the underlying io.ReadCloser.
656 func (rsc *OCIReadSeekCloser) Close() error {
657 rsc.lock.Lock()
658 defer rsc.lock.Unlock()
659 rsc.isClosed = true
660 return nil
661 }
662
663 // Read is a thread-safe operation, it implements io.Read() interface
664 func (rsc *OCIReadSeekCloser) Read(p []byte) (n int, err error) {
665 rsc.lock.Lock()
666 defer rsc.lock.Unlock()
667
668 if rsc.isClosed {
669 return 0, io.EOF
670 }
671
672 return rsc.rc.Read(p)
673 }
674
675 // Seekable is used for check if the binary request body can be seek or no
676 func (rsc *OCIReadSeekCloser) Seekable() bool {
677 if rsc == nil {
678 return false
679 }
680 if _, ok := rsc.rc.(io.Seeker); ok {
681 return true
682 }
683 // once the binary request body is wrapped with ioutil.NopCloser:
684 if isNopCloser(rsc.rc) {
685 if _, ok := reflect.ValueOf(rsc.rc).Field(0).Interface().(io.Seeker); ok {
686 return true
687 }
688 }
689 return false
690 }
691
692 // OCIResponse is the response from issuing a request to an OCI service.
693 type OCIResponse interface {
694 // HTTPResponse returns the raw HTTP response.
695 HTTPResponse() *http.Response
696 }
697
698 // OCIOperation is the generalization of a request-response cycle undergone by an OCI service.
699 type OCIOperation func(context.Context, OCIRequest, *OCIReadSeekCloser, map[string]string) (OCIResponse, error)
700
701 // ClientCallDetails a set of settings used by the a single Call operation of the http Client
702 type ClientCallDetails struct {
703 Signer HTTPRequestSigner
704 }
705
706 // Call executes the http request with the given context
707 func (client BaseClient) Call(ctx context.Context, request *http.Request) (response *http.Response, err error) {
708 if client.IsRefreshableAuthType() {
709 return client.RefreshableTokenWrappedCallWithDetails(ctx, request, ClientCallDetails{Signer: client.Signer})
710 }
711 return client.CallWithDetails(ctx, request, ClientCallDetails{Signer: client.Signer})
712 }
713
714 // RefreshableTokenWrappedCallWithDetails wraps the CallWithDetails with retry on 401 for Refreshable Toekn (Instance Principal, Resource Principal etc.)
715 // This is to intimitate the race condition on refresh
716 func (client BaseClient) RefreshableTokenWrappedCallWithDetails(ctx context.Context, request *http.Request, details ClientCallDetails) (response *http.Response, err error) {
717 for i := 0; i < maxAttemptsForRefreshableRetry; i++ {
718 response, err = client.CallWithDetails(ctx, request, ClientCallDetails{Signer: client.Signer})
719 if response != nil && response.StatusCode != 401 {
720 return response, err
721 }
722 time.Sleep(1 * time.Second)
723 }
724 return
725 }
726
727 // CallWithDetails executes the http request, the given context using details specified in the parameters, this function
728 // provides a way to override some settings present in the client
729 func (client BaseClient) CallWithDetails(ctx context.Context, request *http.Request, details ClientCallDetails) (response *http.Response, err error) {
730 Debugln("Attempting to call downstream service")
731 request = request.WithContext(ctx)
732 err = client.prepareRequest(request)
733 if err != nil {
734 return
735 }
736 //Intercept
737 err = client.intercept(request)
738 if err != nil {
739 return
740 }
741 //Sign the request
742 err = details.Signer.Sign(request)
743 if err != nil {
744 return
745 }
746
747 //Execute the http request
748 if ociGoBreaker := client.Configuration.CircuitBreaker; ociGoBreaker != nil {
749 resp, cbErr := ociGoBreaker.Cb.Execute(func() (interface{}, error) {
750 return client.httpDo(request)
751 })
752 if httpResp, ok := resp.(*http.Response); ok {
753 if httpResp != nil && httpResp.StatusCode != 200 {
754 if failure, ok := IsServiceError(cbErr); ok {
755 ociGoBreaker.AddToHistory(resp.(*http.Response), failure)
756 }
757 }
758 }
759 if cbErr != nil && IsCircuitBreakerError(cbErr) {
760 cbErr = getCircuitBreakerError(request, cbErr, ociGoBreaker)
761 }
762 if _, ok := resp.(*http.Response); !ok {
763 return nil, cbErr
764 }
765 return resp.(*http.Response), cbErr
766 }
767 return client.httpDo(request)
768 }
769
770 // IsRefreshableAuthType validates if a signer is from a refreshable config provider
771 func (client BaseClient) IsRefreshableAuthType() bool {
772 if signer, ok := client.Signer.(ociRequestSigner); ok {
773 if provider, ok := signer.KeyProvider.(RefreshableConfigurationProvider); ok {
774 return provider.Refreshable()
775 }
776 }
777 return false
778 }
779
780 func (client BaseClient) httpDo(request *http.Request) (response *http.Response, err error) {
781
782 //Copy request body and save for logging
783 dumpRequestBody := ioutil.NopCloser(bytes.NewBuffer(nil))
784 if request.Body != nil && !checkBodyLengthExceedLimit(request.ContentLength) {
785 if dumpRequestBody, request.Body, err = drainBody(request.Body); err != nil {
786 dumpRequestBody = ioutil.NopCloser(bytes.NewBuffer(nil))
787 }
788 }
789 IfDebug(func() {
790 logRequest(request, Debugf, verboseLogging)
791 })
792
793 //Execute the http request
794 response, err = client.HTTPClient.Do(request)
795
796 if err != nil {
797 IfInfo(func() {
798 Logf("%v\n", err)
799 })
800 return response, err
801 }
802
803 err = checkForSuccessfulResponse(response, &dumpRequestBody)
804 return response, err
805 }
806
807 // CloseBodyIfValid closes the body of an http response if the response and the body are valid
808 func CloseBodyIfValid(httpResponse *http.Response) {
809 if httpResponse != nil && httpResponse.Body != nil {
810 if httpResponse.Header != nil && strings.ToLower(httpResponse.Header.Get("content-type")) == "text/event-stream" {
811 return
812 }
813 httpResponse.Body.Close()
814 }
815 }
816
817 // IsOciRealmSpecificServiceEndpointTemplateEnabled returns true if the client is configured to use realm specific service endpoint template
818 // it will first check the client configuration, if not set, it will check the environment variable
819 func (client BaseClient) IsOciRealmSpecificServiceEndpointTemplateEnabled() bool {
820 if client.Configuration.RealmSpecificServiceEndpointTemplateEnabled != nil {
821 return *client.Configuration.RealmSpecificServiceEndpointTemplateEnabled
822 }
823 return IsEnvVarTrue(OciRealmSpecificServiceEndpointTemplateEnabledEnvVar)
824 }
825
826 func getCustomCertRefreshInterval() int {
827 if OciGlobalRefreshIntervalForCustomCerts >= 0 {
828 Debugf("Setting refresh interval as %d for custom certs via OciGlobalRefreshIntervalForCustomCerts", OciGlobalRefreshIntervalForCustomCerts)
829 return OciGlobalRefreshIntervalForCustomCerts
830 }
831 if refreshIntervalValue, ok := os.LookupEnv(ociDefaultRefreshIntervalForCustomCerts); ok {
832 refreshInterval, err := strconv.Atoi(refreshIntervalValue)
833 if err != nil || refreshInterval < 0 {
834 Debugf("The environment variable %s is not a valid int or is a negative value, skipping this configuration", ociDefaultRefreshIntervalForCustomCerts)
835 } else {
836 Debugf("Setting refresh interval as %d for custom certs via the env variable %s", refreshInterval, ociDefaultRefreshIntervalForCustomCerts)
837 return refreshInterval
838 }
839 }
840 Debugf("Setting the default refresh interval %d for custom certs", defaultRefreshIntervalForCustomCerts)
841 return defaultRefreshIntervalForCustomCerts
842 }
843