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