380 lines
13 KiB
Go
380 lines
13 KiB
Go
// Copyright (c) 2016, 2018, 2020, Oracle and/or its affiliates. All rights reserved.
|
|
// 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.
|
|
|
|
// Package common provides supporting functions and structs used by service packages
|
|
package common
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"math/rand"
|
|
"net/http"
|
|
"net/http/httputil"
|
|
"net/url"
|
|
"os"
|
|
"os/user"
|
|
"path"
|
|
"runtime"
|
|
"strings"
|
|
"sync/atomic"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
// DefaultHostURLTemplate The default url template for service hosts
|
|
DefaultHostURLTemplate = "%s.%s.oraclecloud.com"
|
|
|
|
// requestHeaderAccept The key for passing a header to indicate Accept
|
|
requestHeaderAccept = "Accept"
|
|
|
|
// requestHeaderAuthorization The key for passing a header to indicate Authorization
|
|
requestHeaderAuthorization = "Authorization"
|
|
|
|
// requestHeaderContentLength The key for passing a header to indicate Content Length
|
|
requestHeaderContentLength = "Content-Length"
|
|
|
|
// requestHeaderContentType The key for passing a header to indicate Content Type
|
|
requestHeaderContentType = "Content-Type"
|
|
|
|
// requestHeaderDate The key for passing a header to indicate Date
|
|
requestHeaderDate = "Date"
|
|
|
|
// requestHeaderIfMatch The key for passing a header to indicate If Match
|
|
requestHeaderIfMatch = "if-match"
|
|
|
|
// requestHeaderOpcClientInfo The key for passing a header to indicate OPC Client Info
|
|
requestHeaderOpcClientInfo = "opc-client-info"
|
|
|
|
// requestHeaderOpcRetryToken The key for passing a header to indicate OPC Retry Token
|
|
requestHeaderOpcRetryToken = "opc-retry-token"
|
|
|
|
// requestHeaderOpcRequestID The key for unique Oracle-assigned identifier for the request.
|
|
requestHeaderOpcRequestID = "opc-request-id"
|
|
|
|
// requestHeaderOpcClientRequestID The key for unique Oracle-assigned identifier for the request.
|
|
requestHeaderOpcClientRequestID = "opc-client-request-id"
|
|
|
|
// requestHeaderUserAgent The key for passing a header to indicate User Agent
|
|
requestHeaderUserAgent = "User-Agent"
|
|
|
|
// requestHeaderXContentSHA256 The key for passing a header to indicate SHA256 hash
|
|
requestHeaderXContentSHA256 = "X-Content-SHA256"
|
|
|
|
// requestHeaderOpcOboToken The key for passing a header to use obo token
|
|
requestHeaderOpcOboToken = "opc-obo-token"
|
|
|
|
// private constants
|
|
defaultScheme = "https"
|
|
defaultSDKMarker = "Oracle-GoSDK"
|
|
defaultUserAgentTemplate = "%s/%s (%s/%s; go/%s)" //SDK/SDKVersion (OS/OSVersion; Lang/LangVersion)
|
|
defaultTimeout = 60 * time.Second
|
|
defaultConfigFileName = "config"
|
|
defaultConfigDirName = ".oci"
|
|
secondaryConfigDirName = ".oraclebmc"
|
|
maxBodyLenForDebug = 1024 * 1000
|
|
)
|
|
|
|
// RequestInterceptor function used to customize the request before calling the underlying service
|
|
type RequestInterceptor func(*http.Request) error
|
|
|
|
// HTTPRequestDispatcher wraps the execution of a http request, it is generally implemented by
|
|
// http.Client.Do, but can be customized for testing
|
|
type HTTPRequestDispatcher interface {
|
|
Do(req *http.Request) (*http.Response, error)
|
|
}
|
|
|
|
// BaseClient struct implements all basic operations to call oci web services.
|
|
type BaseClient struct {
|
|
//HTTPClient performs the http network operations
|
|
HTTPClient HTTPRequestDispatcher
|
|
|
|
//Signer performs auth operation
|
|
Signer HTTPRequestSigner
|
|
|
|
//A request interceptor can be used to customize the request before signing and dispatching
|
|
Interceptor RequestInterceptor
|
|
|
|
//The host of the service
|
|
Host string
|
|
|
|
//The user agent
|
|
UserAgent string
|
|
|
|
//Base path for all operations of this client
|
|
BasePath string
|
|
}
|
|
|
|
func defaultUserAgent() string {
|
|
userAgent := fmt.Sprintf(defaultUserAgentTemplate, defaultSDKMarker, Version(), runtime.GOOS, runtime.GOARCH, runtime.Version())
|
|
return userAgent
|
|
}
|
|
|
|
var clientCounter int64
|
|
|
|
func getNextSeed() int64 {
|
|
newCounterValue := atomic.AddInt64(&clientCounter, 1)
|
|
return newCounterValue + time.Now().UnixNano()
|
|
}
|
|
|
|
func newBaseClient(signer HTTPRequestSigner, dispatcher HTTPRequestDispatcher) BaseClient {
|
|
rand.Seed(getNextSeed())
|
|
return BaseClient{
|
|
UserAgent: defaultUserAgent(),
|
|
Interceptor: nil,
|
|
Signer: signer,
|
|
HTTPClient: dispatcher,
|
|
}
|
|
}
|
|
|
|
func defaultHTTPDispatcher() http.Client {
|
|
httpClient := http.Client{
|
|
Timeout: defaultTimeout,
|
|
}
|
|
return httpClient
|
|
}
|
|
|
|
func defaultBaseClient(provider KeyProvider) BaseClient {
|
|
dispatcher := defaultHTTPDispatcher()
|
|
signer := DefaultRequestSigner(provider)
|
|
return newBaseClient(signer, &dispatcher)
|
|
}
|
|
|
|
//DefaultBaseClientWithSigner creates a default base client with a given signer
|
|
func DefaultBaseClientWithSigner(signer HTTPRequestSigner) BaseClient {
|
|
dispatcher := defaultHTTPDispatcher()
|
|
return newBaseClient(signer, &dispatcher)
|
|
}
|
|
|
|
// NewClientWithConfig Create a new client with a configuration provider, the configuration provider
|
|
// will be used for the default signer as well as reading the region
|
|
// This function does not check for valid regions to implement forward compatibility
|
|
func NewClientWithConfig(configProvider ConfigurationProvider) (client BaseClient, err error) {
|
|
var ok bool
|
|
if ok, err = IsConfigurationProviderValid(configProvider); !ok {
|
|
err = fmt.Errorf("can not create client, bad configuration: %s", err.Error())
|
|
return
|
|
}
|
|
|
|
client = defaultBaseClient(configProvider)
|
|
|
|
return
|
|
}
|
|
|
|
// NewClientWithOboToken Create a new client that will use oboToken for auth
|
|
func NewClientWithOboToken(configProvider ConfigurationProvider, oboToken string) (client BaseClient, err error) {
|
|
client, err = NewClientWithConfig(configProvider)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
// Interceptor to add obo token header
|
|
client.Interceptor = func(request *http.Request) error {
|
|
request.Header.Add(requestHeaderOpcOboToken, oboToken)
|
|
return nil
|
|
}
|
|
// Obo token will also be signed
|
|
defaultHeaders := append(DefaultGenericHeaders(), requestHeaderOpcOboToken)
|
|
client.Signer = RequestSigner(configProvider, defaultHeaders, DefaultBodyHeaders())
|
|
|
|
return
|
|
}
|
|
|
|
func getHomeFolder() string {
|
|
current, e := user.Current()
|
|
if e != nil {
|
|
//Give up and try to return something sensible
|
|
home := os.Getenv("HOME")
|
|
if home == "" {
|
|
home = os.Getenv("USERPROFILE")
|
|
}
|
|
return home
|
|
}
|
|
return current.HomeDir
|
|
}
|
|
|
|
// DefaultConfigProvider returns the default config provider. The default config provider
|
|
// will look for configurations in 3 places: file in $HOME/.oci/config, HOME/.obmcs/config and
|
|
// variables names starting with the string TF_VAR. If the same configuration is found in multiple
|
|
// places the provider will prefer the first one.
|
|
func DefaultConfigProvider() ConfigurationProvider {
|
|
homeFolder := getHomeFolder()
|
|
defaultConfigFile := path.Join(homeFolder, defaultConfigDirName, defaultConfigFileName)
|
|
secondaryConfigFile := path.Join(homeFolder, secondaryConfigDirName, defaultConfigFileName)
|
|
|
|
defaultFileProvider, _ := ConfigurationProviderFromFile(defaultConfigFile, "")
|
|
secondaryFileProvider, _ := ConfigurationProviderFromFile(secondaryConfigFile, "")
|
|
environmentProvider := environmentConfigurationProvider{EnvironmentVariablePrefix: "TF_VAR"}
|
|
|
|
provider, _ := ComposingConfigurationProvider([]ConfigurationProvider{defaultFileProvider, secondaryFileProvider, environmentProvider})
|
|
Debugf("Configuration provided by: %s", provider)
|
|
return provider
|
|
}
|
|
|
|
// CustomProfileConfigProvider returns the config provider of given profile. The custom profile config provider
|
|
// will look for configurations in 2 places: file in $HOME/.oci/config, and variables names starting with the
|
|
// string TF_VAR. If the same configuration is found in multiple places the provider will prefer the first one.
|
|
func CustomProfileConfigProvider(customConfigPath string, profile string) ConfigurationProvider {
|
|
homeFolder := getHomeFolder()
|
|
if customConfigPath == "" {
|
|
customConfigPath = path.Join(homeFolder, defaultConfigDirName, defaultConfigFileName)
|
|
}
|
|
customFileProvider, _ := ConfigurationProviderFromFileWithProfile(customConfigPath, profile, "")
|
|
defaultFileProvider, _ := ConfigurationProviderFromFileWithProfile(customConfigPath, "DEFAULT", "")
|
|
environmentProvider := environmentConfigurationProvider{EnvironmentVariablePrefix: "TF_VAR"}
|
|
provider, _ := ComposingConfigurationProvider([]ConfigurationProvider{customFileProvider, defaultFileProvider, environmentProvider})
|
|
Debugf("Configuration provided by: %s", provider)
|
|
return provider
|
|
}
|
|
|
|
func (client *BaseClient) prepareRequest(request *http.Request) (err error) {
|
|
if client.UserAgent == "" {
|
|
return fmt.Errorf("user agent can not be blank")
|
|
}
|
|
|
|
if request.Header == nil {
|
|
request.Header = http.Header{}
|
|
}
|
|
request.Header.Set(requestHeaderUserAgent, client.UserAgent)
|
|
request.Header.Set(requestHeaderDate, time.Now().UTC().Format(http.TimeFormat))
|
|
|
|
if !strings.Contains(client.Host, "http") &&
|
|
!strings.Contains(client.Host, "https") {
|
|
client.Host = fmt.Sprintf("%s://%s", defaultScheme, client.Host)
|
|
}
|
|
|
|
clientURL, err := url.Parse(client.Host)
|
|
if err != nil {
|
|
return fmt.Errorf("host is invalid. %s", err.Error())
|
|
}
|
|
request.URL.Host = clientURL.Host
|
|
request.URL.Scheme = clientURL.Scheme
|
|
currentPath := request.URL.Path
|
|
if !strings.Contains(currentPath, fmt.Sprintf("/%s", client.BasePath)) {
|
|
request.URL.Path = path.Clean(fmt.Sprintf("/%s/%s", client.BasePath, currentPath))
|
|
}
|
|
return
|
|
}
|
|
|
|
func (client BaseClient) intercept(request *http.Request) (err error) {
|
|
if client.Interceptor != nil {
|
|
err = client.Interceptor(request)
|
|
}
|
|
return
|
|
}
|
|
|
|
func checkForSuccessfulResponse(res *http.Response) error {
|
|
familyStatusCode := res.StatusCode / 100
|
|
if familyStatusCode == 4 || familyStatusCode == 5 {
|
|
return newServiceFailureFromResponse(res)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// OCIRequest is any request made to an OCI service.
|
|
type OCIRequest interface {
|
|
// HTTPRequest assembles an HTTP request.
|
|
HTTPRequest(method, path string) (http.Request, error)
|
|
}
|
|
|
|
// RequestMetadata is metadata about an OCIRequest. This structure represents the behavior exhibited by the SDK when
|
|
// issuing (or reissuing) a request.
|
|
type RequestMetadata struct {
|
|
// RetryPolicy is the policy for reissuing the request. If no retry policy is set on the request,
|
|
// then the request will be issued exactly once.
|
|
RetryPolicy *RetryPolicy
|
|
}
|
|
|
|
// OCIResponse is the response from issuing a request to an OCI service.
|
|
type OCIResponse interface {
|
|
// HTTPResponse returns the raw HTTP response.
|
|
HTTPResponse() *http.Response
|
|
}
|
|
|
|
// OCIOperation is the generalization of a request-response cycle undergone by an OCI service.
|
|
type OCIOperation func(context.Context, OCIRequest) (OCIResponse, error)
|
|
|
|
//ClientCallDetails a set of settings used by the a single Call operation of the http Client
|
|
type ClientCallDetails struct {
|
|
Signer HTTPRequestSigner
|
|
}
|
|
|
|
// Call executes the http request with the given context
|
|
func (client BaseClient) Call(ctx context.Context, request *http.Request) (response *http.Response, err error) {
|
|
return client.CallWithDetails(ctx, request, ClientCallDetails{Signer: client.Signer})
|
|
}
|
|
|
|
// CallWithDetails executes the http request, the given context using details specified in the paremeters, this function
|
|
// provides a way to override some settings present in the client
|
|
func (client BaseClient) CallWithDetails(ctx context.Context, request *http.Request, details ClientCallDetails) (response *http.Response, err error) {
|
|
Debugln("Atempting to call downstream service")
|
|
request = request.WithContext(ctx)
|
|
|
|
err = client.prepareRequest(request)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
//Intercept
|
|
err = client.intercept(request)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
//Sign the request
|
|
err = details.Signer.Sign(request)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
IfDebug(func() {
|
|
dumpBody := true
|
|
if request.ContentLength > maxBodyLenForDebug {
|
|
Debugf("not dumping body too big\n")
|
|
dumpBody = false
|
|
}
|
|
dumpBody = dumpBody && defaultLogger.LogLevel() == verboseLogging
|
|
if dump, e := httputil.DumpRequestOut(request, dumpBody); e == nil {
|
|
Debugf("Dump Request %s", string(dump))
|
|
} else {
|
|
Debugf("%v\n", e)
|
|
}
|
|
})
|
|
|
|
//Execute the http request
|
|
response, err = client.HTTPClient.Do(request)
|
|
|
|
IfDebug(func() {
|
|
if err != nil {
|
|
Debugf("%v\n", err)
|
|
return
|
|
}
|
|
|
|
dumpBody := true
|
|
if response.ContentLength > maxBodyLenForDebug {
|
|
Debugf("not dumping body too big\n")
|
|
dumpBody = false
|
|
}
|
|
|
|
dumpBody = dumpBody && defaultLogger.LogLevel() == verboseLogging
|
|
if dump, e := httputil.DumpResponse(response, dumpBody); e == nil {
|
|
Debugf("Dump Response %s", string(dump))
|
|
} else {
|
|
Debugf("%v\n", e)
|
|
}
|
|
})
|
|
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
err = checkForSuccessfulResponse(response)
|
|
return
|
|
}
|
|
|
|
//CloseBodyIfValid closes the body of an http response if the response and the body are valid
|
|
func CloseBodyIfValid(httpResponse *http.Response) {
|
|
if httpResponse != nil && httpResponse.Body != nil {
|
|
httpResponse.Body.Close()
|
|
}
|
|
}
|