1// Copyright (c) 2016, 2018, 2020, 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 5package common 6 7import ( 8 "context" 9 "fmt" 10 "math/rand" 11 "net/http" 12 "net/http/httputil" 13 "net/url" 14 "os" 15 "os/user" 16 "path" 17 "runtime" 18 "strings" 19 "sync/atomic" 20 "time" 21) 22 23const ( 24 // DefaultHostURLTemplate The default url template for service hosts 25 DefaultHostURLTemplate = "%s.%s.oraclecloud.com" 26 27 // requestHeaderAccept The key for passing a header to indicate Accept 28 requestHeaderAccept = "Accept" 29 30 // requestHeaderAuthorization The key for passing a header to indicate Authorization 31 requestHeaderAuthorization = "Authorization" 32 33 // requestHeaderContentLength The key for passing a header to indicate Content Length 34 requestHeaderContentLength = "Content-Length" 35 36 // requestHeaderContentType The key for passing a header to indicate Content Type 37 requestHeaderContentType = "Content-Type" 38 39 // requestHeaderDate The key for passing a header to indicate Date 40 requestHeaderDate = "Date" 41 42 // requestHeaderIfMatch The key for passing a header to indicate If Match 43 requestHeaderIfMatch = "if-match" 44 45 // requestHeaderOpcClientInfo The key for passing a header to indicate OPC Client Info 46 requestHeaderOpcClientInfo = "opc-client-info" 47 48 // requestHeaderOpcRetryToken The key for passing a header to indicate OPC Retry Token 49 requestHeaderOpcRetryToken = "opc-retry-token" 50 51 // requestHeaderOpcRequestID The key for unique Oracle-assigned identifier for the request. 52 requestHeaderOpcRequestID = "opc-request-id" 53 54 // requestHeaderOpcClientRequestID The key for unique Oracle-assigned identifier for the request. 55 requestHeaderOpcClientRequestID = "opc-client-request-id" 56 57 // requestHeaderUserAgent The key for passing a header to indicate User Agent 58 requestHeaderUserAgent = "User-Agent" 59 60 // requestHeaderXContentSHA256 The key for passing a header to indicate SHA256 hash 61 requestHeaderXContentSHA256 = "X-Content-SHA256" 62 63 // requestHeaderOpcOboToken The key for passing a header to use obo token 64 requestHeaderOpcOboToken = "opc-obo-token" 65 66 // private constants 67 defaultScheme = "https" 68 defaultSDKMarker = "Oracle-GoSDK" 69 defaultUserAgentTemplate = "%s/%s (%s/%s; go/%s)" //SDK/SDKVersion (OS/OSVersion; Lang/LangVersion) 70 defaultTimeout = 60 * time.Second 71 defaultConfigFileName = "config" 72 defaultConfigDirName = ".oci" 73 configFilePathEnvVarName = "OCI_CONFIG_FILE" 74 secondaryConfigDirName = ".oraclebmc" 75 maxBodyLenForDebug = 1024 * 1000 76) 77 78// RequestInterceptor function used to customize the request before calling the underlying service 79type RequestInterceptor func(*http.Request) error 80 81// HTTPRequestDispatcher wraps the execution of a http request, it is generally implemented by 82// http.Client.Do, but can be customized for testing 83type HTTPRequestDispatcher interface { 84 Do(req *http.Request) (*http.Response, error) 85} 86 87// BaseClient struct implements all basic operations to call oci web services. 88type BaseClient struct { 89 //HTTPClient performs the http network operations 90 HTTPClient HTTPRequestDispatcher 91 92 //Signer performs auth operation 93 Signer HTTPRequestSigner 94 95 //A request interceptor can be used to customize the request before signing and dispatching 96 Interceptor RequestInterceptor 97 98 //The host of the service 99 Host string 100 101 //The user agent 102 UserAgent string 103 104 //Base path for all operations of this client 105 BasePath string 106} 107 108func defaultUserAgent() string { 109 userAgent := fmt.Sprintf(defaultUserAgentTemplate, defaultSDKMarker, Version(), runtime.GOOS, runtime.GOARCH, runtime.Version()) 110 return userAgent 111} 112 113var clientCounter int64 114 115func getNextSeed() int64 { 116 newCounterValue := atomic.AddInt64(&clientCounter, 1) 117 return newCounterValue + time.Now().UnixNano() 118} 119 120func newBaseClient(signer HTTPRequestSigner, dispatcher HTTPRequestDispatcher) BaseClient { 121 rand.Seed(getNextSeed()) 122 return BaseClient{ 123 UserAgent: defaultUserAgent(), 124 Interceptor: nil, 125 Signer: signer, 126 HTTPClient: dispatcher, 127 } 128} 129 130func defaultHTTPDispatcher() http.Client { 131 httpClient := http.Client{ 132 Timeout: defaultTimeout, 133 } 134 return httpClient 135} 136 137func defaultBaseClient(provider KeyProvider) BaseClient { 138 dispatcher := defaultHTTPDispatcher() 139 signer := DefaultRequestSigner(provider) 140 return newBaseClient(signer, &dispatcher) 141} 142 143//DefaultBaseClientWithSigner creates a default base client with a given signer 144func DefaultBaseClientWithSigner(signer HTTPRequestSigner) BaseClient { 145 dispatcher := defaultHTTPDispatcher() 146 return newBaseClient(signer, &dispatcher) 147} 148 149// NewClientWithConfig Create a new client with a configuration provider, the configuration provider 150// will be used for the default signer as well as reading the region 151// This function does not check for valid regions to implement forward compatibility 152func NewClientWithConfig(configProvider ConfigurationProvider) (client BaseClient, err error) { 153 var ok bool 154 if ok, err = IsConfigurationProviderValid(configProvider); !ok { 155 err = fmt.Errorf("can not create client, bad configuration: %s", err.Error()) 156 return 157 } 158 159 client = defaultBaseClient(configProvider) 160 161 return 162} 163 164// NewClientWithOboToken Create a new client that will use oboToken for auth 165func NewClientWithOboToken(configProvider ConfigurationProvider, oboToken string) (client BaseClient, err error) { 166 client, err = NewClientWithConfig(configProvider) 167 if err != nil { 168 return 169 } 170 171 // Interceptor to add obo token header 172 client.Interceptor = func(request *http.Request) error { 173 request.Header.Add(requestHeaderOpcOboToken, oboToken) 174 return nil 175 } 176 // Obo token will also be signed 177 defaultHeaders := append(DefaultGenericHeaders(), requestHeaderOpcOboToken) 178 client.Signer = RequestSigner(configProvider, defaultHeaders, DefaultBodyHeaders()) 179 180 return 181} 182 183func getHomeFolder() string { 184 current, e := user.Current() 185 if e != nil { 186 //Give up and try to return something sensible 187 home := os.Getenv("HOME") 188 if home == "" { 189 home = os.Getenv("USERPROFILE") 190 } 191 return home 192 } 193 return current.HomeDir 194} 195 196// DefaultConfigProvider returns the default config provider. The default config provider 197// will look for configurations in 3 places: file in $HOME/.oci/config, HOME/.obmcs/config and 198// variables names starting with the string TF_VAR. If the same configuration is found in multiple 199// places the provider will prefer the first one. 200// If the config file is not placed in the default location, the environment variable 201// OCI_CONFIG_FILE can provide the config file location. 202func DefaultConfigProvider() ConfigurationProvider { 203 defaultConfigFile := getDefaultConfigFilePath() 204 homeFolder := getHomeFolder() 205 secondaryConfigFile := path.Join(homeFolder, secondaryConfigDirName, defaultConfigFileName) 206 207 defaultFileProvider, _ := ConfigurationProviderFromFile(defaultConfigFile, "") 208 secondaryFileProvider, _ := ConfigurationProviderFromFile(secondaryConfigFile, "") 209 environmentProvider := environmentConfigurationProvider{EnvironmentVariablePrefix: "TF_VAR"} 210 211 provider, _ := ComposingConfigurationProvider([]ConfigurationProvider{defaultFileProvider, secondaryFileProvider, environmentProvider}) 212 Debugf("Configuration provided by: %s", provider) 213 return provider 214} 215 216func getDefaultConfigFilePath() string { 217 homeFolder := getHomeFolder() 218 defaultConfigFile := path.Join(homeFolder, defaultConfigDirName, defaultConfigFileName) 219 if _, err := os.Stat(defaultConfigFile); err == nil { 220 return defaultConfigFile 221 } 222 Debugf("The %s does not exist, will check env var %s for file path.", defaultConfigFile, configFilePathEnvVarName) 223 // Read configuration file path from OCI_CONFIG_FILE env var 224 fallbackConfigFile, existed := os.LookupEnv(configFilePathEnvVarName) 225 if !existed { 226 Debugf("The env var %s does not exist...", configFilePathEnvVarName) 227 return defaultConfigFile 228 } 229 if _, err := os.Stat(fallbackConfigFile); os.IsNotExist(err) { 230 Debugf("The specified cfg file path in the env var %s does not exist: %s", configFilePathEnvVarName, fallbackConfigFile) 231 return defaultConfigFile 232 } 233 return fallbackConfigFile 234} 235 236// CustomProfileConfigProvider returns the config provider of given profile. The custom profile config provider 237// will look for configurations in 2 places: file in $HOME/.oci/config, and variables names starting with the 238// string TF_VAR. If the same configuration is found in multiple places the provider will prefer the first one. 239func CustomProfileConfigProvider(customConfigPath string, profile string) ConfigurationProvider { 240 homeFolder := getHomeFolder() 241 if customConfigPath == "" { 242 customConfigPath = path.Join(homeFolder, defaultConfigDirName, defaultConfigFileName) 243 } 244 customFileProvider, _ := ConfigurationProviderFromFileWithProfile(customConfigPath, profile, "") 245 defaultFileProvider, _ := ConfigurationProviderFromFileWithProfile(customConfigPath, "DEFAULT", "") 246 environmentProvider := environmentConfigurationProvider{EnvironmentVariablePrefix: "TF_VAR"} 247 provider, _ := ComposingConfigurationProvider([]ConfigurationProvider{customFileProvider, defaultFileProvider, environmentProvider}) 248 Debugf("Configuration provided by: %s", provider) 249 return provider 250} 251 252func (client *BaseClient) prepareRequest(request *http.Request) (err error) { 253 if client.UserAgent == "" { 254 return fmt.Errorf("user agent can not be blank") 255 } 256 257 if request.Header == nil { 258 request.Header = http.Header{} 259 } 260 request.Header.Set(requestHeaderUserAgent, client.UserAgent) 261 request.Header.Set(requestHeaderDate, time.Now().UTC().Format(http.TimeFormat)) 262 263 if !strings.Contains(client.Host, "http") && 264 !strings.Contains(client.Host, "https") { 265 client.Host = fmt.Sprintf("%s://%s", defaultScheme, client.Host) 266 } 267 268 clientURL, err := url.Parse(client.Host) 269 if err != nil { 270 return fmt.Errorf("host is invalid. %s", err.Error()) 271 } 272 request.URL.Host = clientURL.Host 273 request.URL.Scheme = clientURL.Scheme 274 currentPath := request.URL.Path 275 if !strings.Contains(currentPath, fmt.Sprintf("/%s", client.BasePath)) { 276 request.URL.Path = path.Clean(fmt.Sprintf("/%s/%s", client.BasePath, currentPath)) 277 } 278 return 279} 280 281func (client BaseClient) intercept(request *http.Request) (err error) { 282 if client.Interceptor != nil { 283 err = client.Interceptor(request) 284 } 285 return 286} 287 288func checkForSuccessfulResponse(res *http.Response) error { 289 familyStatusCode := res.StatusCode / 100 290 if familyStatusCode == 4 || familyStatusCode == 5 { 291 return newServiceFailureFromResponse(res) 292 } 293 return nil 294} 295 296// OCIRequest is any request made to an OCI service. 297type OCIRequest interface { 298 // HTTPRequest assembles an HTTP request. 299 HTTPRequest(method, path string) (http.Request, error) 300} 301 302// RequestMetadata is metadata about an OCIRequest. This structure represents the behavior exhibited by the SDK when 303// issuing (or reissuing) a request. 304type RequestMetadata struct { 305 // RetryPolicy is the policy for reissuing the request. If no retry policy is set on the request, 306 // then the request will be issued exactly once. 307 RetryPolicy *RetryPolicy 308} 309 310// OCIResponse is the response from issuing a request to an OCI service. 311type OCIResponse interface { 312 // HTTPResponse returns the raw HTTP response. 313 HTTPResponse() *http.Response 314} 315 316// OCIOperation is the generalization of a request-response cycle undergone by an OCI service. 317type OCIOperation func(context.Context, OCIRequest) (OCIResponse, error) 318 319//ClientCallDetails a set of settings used by the a single Call operation of the http Client 320type ClientCallDetails struct { 321 Signer HTTPRequestSigner 322} 323 324// Call executes the http request with the given context 325func (client BaseClient) Call(ctx context.Context, request *http.Request) (response *http.Response, err error) { 326 return client.CallWithDetails(ctx, request, ClientCallDetails{Signer: client.Signer}) 327} 328 329// CallWithDetails executes the http request, the given context using details specified in the paremeters, this function 330// provides a way to override some settings present in the client 331func (client BaseClient) CallWithDetails(ctx context.Context, request *http.Request, details ClientCallDetails) (response *http.Response, err error) { 332 Debugln("Atempting to call downstream service") 333 request = request.WithContext(ctx) 334 335 err = client.prepareRequest(request) 336 if err != nil { 337 return 338 } 339 340 //Intercept 341 err = client.intercept(request) 342 if err != nil { 343 return 344 } 345 346 //Sign the request 347 err = details.Signer.Sign(request) 348 if err != nil { 349 return 350 } 351 352 IfDebug(func() { 353 dumpBody := true 354 if request.ContentLength > maxBodyLenForDebug { 355 Debugf("not dumping body too big\n") 356 dumpBody = false 357 } 358 dumpBody = dumpBody && defaultLogger.LogLevel() == verboseLogging 359 if dump, e := httputil.DumpRequestOut(request, dumpBody); e == nil { 360 Debugf("Dump Request %s", string(dump)) 361 } else { 362 Debugf("%v\n", e) 363 } 364 }) 365 366 //Execute the http request 367 response, err = client.HTTPClient.Do(request) 368 369 IfDebug(func() { 370 if err != nil { 371 Debugf("%v\n", err) 372 return 373 } 374 375 dumpBody := true 376 if response.ContentLength > maxBodyLenForDebug { 377 Debugf("not dumping body too big\n") 378 dumpBody = false 379 } 380 381 dumpBody = dumpBody && defaultLogger.LogLevel() == verboseLogging 382 if dump, e := httputil.DumpResponse(response, dumpBody); e == nil { 383 Debugf("Dump Response %s", string(dump)) 384 } else { 385 Debugf("%v\n", e) 386 } 387 }) 388 389 if err != nil { 390 return 391 } 392 393 err = checkForSuccessfulResponse(response) 394 return 395} 396 397//CloseBodyIfValid closes the body of an http response if the response and the body are valid 398func CloseBodyIfValid(httpResponse *http.Response) { 399 if httpResponse != nil && httpResponse.Body != nil { 400 httpResponse.Body.Close() 401 } 402} 403