1/**
2 * Copyright 2016 IBM Corp.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *    http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package session
18
19import (
20	"context"
21	"fmt"
22	"log"
23	"math/rand"
24	"net"
25	"net/http"
26	"os"
27	"os/user"
28	"runtime"
29	"strings"
30	"time"
31
32	"github.com/softlayer/softlayer-go/config"
33	"github.com/softlayer/softlayer-go/sl"
34)
35
36// Logger is the logger used by the SoftLayer session package. Can be overridden by the user.
37var Logger *log.Logger
38
39func init() {
40	// initialize the logger used by the session package.
41	Logger = log.New(os.Stderr, "", log.LstdFlags)
42}
43
44// DefaultEndpoint is the default endpoint for API calls, when no override
45// is provided.
46const DefaultEndpoint = "https://api.softlayer.com/rest/v3"
47
48var retryableErrorCodes = []string{"SoftLayer_Exception_WebService_RateLimitExceeded"}
49
50// TransportHandler interface for the protocol-specific handling of API requests.
51type TransportHandler interface {
52	// DoRequest is the protocol-specific handler for making API requests.
53	//
54	// sess is a reference to the current session object, where authentication and
55	// endpoint information can be found.
56	//
57	// service and method are the SoftLayer service name and method name, exactly as they
58	// are documented at http://sldn.softlayer.com/reference/softlayerapi (i.e., with the
59	// 'SoftLayer_' prefix and properly cased.
60	//
61	// args is a slice of arguments required for the service method being invoked.  The
62	// types of each argument varies. See the method definition in the services package
63	// for the expected type of each argument.
64	//
65	// options is an sl.Options struct, containing any mask, filter, or result limit values
66	// to be applied.
67	//
68	// pResult is a pointer to a variable to be populated with the result of the API call.
69	// DoRequest should ensure that the native API response (i.e., XML or JSON) is correctly
70	// unmarshaled into the result structure.
71	//
72	// A sl.Error is returned, and can be (with a type assertion) inspected for details of
73	// the error (http code, API error message, etc.), or simply handled as a generic error,
74	// (in which case no type assertion would be necessary)
75	DoRequest(
76		sess *Session,
77		service string,
78		method string,
79		args []interface{},
80		options *sl.Options,
81		pResult interface{}) error
82}
83
84const (
85	DefaultTimeout   = time.Second * 120
86	DefaultRetryWait = time.Second * 3
87)
88
89// Session stores the information required for communication with the SoftLayer
90// API
91type Session struct {
92	// UserName is the name of the SoftLayer API user
93	UserName string
94
95	// ApiKey is the secret for making API calls
96	APIKey string
97
98	// Endpoint is the SoftLayer API endpoint to communicate with
99	Endpoint string
100
101	// UserId is the user id for token-based authentication
102	UserId int
103
104	//IAMToken is the IAM token secret that included IMS account for token-based authentication
105	IAMToken string
106
107	// AuthToken is the token secret for token-based authentication
108	AuthToken string
109
110	// Debug controls logging of request details (URI, parameters, etc.)
111	Debug bool
112
113	// The handler whose DoRequest() function will be called for each API request.
114	// Handles the request and any response parsing specific to the desired protocol
115	// (e.g., REST).  Set automatically for a new Session, based on the
116	// provided Endpoint.
117	TransportHandler TransportHandler
118
119	// HTTPClient This allows a custom user configured HTTP Client.
120	HTTPClient *http.Client
121
122	// Context allows a custom context.Context for outbound HTTP requests
123	Context context.Context
124
125	// Custom Headers to be used on each request (Currently only for rest)
126	Headers map[string]string
127
128	// Timeout specifies a time limit for http requests made by this
129	// session. Requests that take longer that the specified timeout
130	// will result in an error.
131	Timeout time.Duration
132
133	// Retries is the number of times to retry a connection that failed due to a timeout.
134	Retries int
135
136	// RetryWait minimum wait time to retry a request
137	RetryWait time.Duration
138
139	// userAgent is the user agent to send with each API request
140	// User shouldn't be able to change or set the base user agent
141	userAgent string
142}
143
144func init() {
145	rand.Seed(time.Now().UnixNano())
146}
147
148// New creates and returns a pointer to a new session object.  It takes up to
149// four parameters, all of which are optional.  If specified, they will be
150// interpreted in the following sequence:
151//
152// 1. UserName
153// 2. Api Key
154// 3. Endpoint
155// 4. Timeout
156//
157// If one or more are omitted, New() will attempt to retrieve these values from
158// the environment, and the ~/.softlayer config file, in that order.
159func New(args ...interface{}) *Session {
160	keys := map[string]int{"username": 0, "api_key": 1, "endpoint_url": 2, "timeout": 3}
161	values := []string{"", "", "", ""}
162
163	for i := 0; i < len(args); i++ {
164		values[i] = args[i].(string)
165	}
166
167	// Default to the environment variables
168
169	// Prioritize SL_USERNAME
170	envFallback("SL_USERNAME", &values[keys["username"]])
171	envFallback("SOFTLAYER_USERNAME", &values[keys["username"]])
172
173	// Prioritize SL_API_KEY
174	envFallback("SL_API_KEY", &values[keys["api_key"]])
175	envFallback("SOFTLAYER_API_KEY", &values[keys["api_key"]])
176
177	// Prioritize SL_ENDPOINT_URL
178	envFallback("SL_ENDPOINT_URL", &values[keys["endpoint_url"]])
179	envFallback("SOFTLAYER_ENDPOINT_URL", &values[keys["endpoint_url"]])
180
181	envFallback("SL_TIMEOUT", &values[keys["timeout"]])
182	envFallback("SOFTLAYER_TIMEOUT", &values[keys["timeout"]])
183
184	// Read ~/.softlayer for configuration
185	var homeDir string
186	u, err := user.Current()
187	if err != nil {
188		for _, name := range []string{"HOME", "USERPROFILE"} { // *nix, windows
189			if dir := os.Getenv(name); dir != "" {
190				homeDir = dir
191				break
192			}
193		}
194	} else {
195		homeDir = u.HomeDir
196	}
197
198	if homeDir != "" {
199		configPath := fmt.Sprintf("%s/.softlayer", homeDir)
200		if _, err = os.Stat(configPath); !os.IsNotExist(err) {
201			// config file exists
202			file, err := config.LoadFile(configPath)
203			if err != nil {
204				log.Println(fmt.Sprintf("[WARN] session: Could not parse %s : %s", configPath, err))
205			} else {
206				for k, v := range keys {
207					value, ok := file.Get("softlayer", k)
208					if ok && values[v] == "" {
209						values[v] = value
210					}
211				}
212			}
213		}
214	} else {
215		log.Println("[WARN] session: home dir could not be determined. Skipping read of ~/.softlayer.")
216	}
217
218	endpointURL := values[keys["endpoint_url"]]
219	if endpointURL == "" {
220		endpointURL = DefaultEndpoint
221	}
222
223	sess := &Session{
224		UserName:  values[keys["username"]],
225		APIKey:    values[keys["api_key"]],
226		Endpoint:  endpointURL,
227		userAgent: getDefaultUserAgent(),
228	}
229
230	timeout := values[keys["timeout"]]
231	if timeout != "" {
232		timeoutDuration, err := time.ParseDuration(fmt.Sprintf("%ss", timeout))
233		if err == nil {
234			sess.Timeout = timeoutDuration
235		}
236	}
237
238	sess.RetryWait = DefaultRetryWait
239
240	return sess
241}
242
243// DoRequest hands off the processing to the assigned transport handler. It is
244// normally called internally by the service objects, but is exported so that it can
245// be invoked directly by client code in exceptional cases where direct control is
246// needed over one of the parameters.
247//
248// For a description of parameters, see TransportHandler.DoRequest in this package
249func (r *Session) DoRequest(service string, method string, args []interface{}, options *sl.Options, pResult interface{}) error {
250	if r.TransportHandler == nil {
251		r.TransportHandler = getDefaultTransport(r.Endpoint)
252	}
253
254	return r.TransportHandler.DoRequest(r, service, method, args, options, pResult)
255}
256
257// SetTimeout creates a copy of the session and sets the passed timeout into it
258// before returning it.
259func (r *Session) SetTimeout(timeout time.Duration) *Session {
260	var s Session
261	s = *r
262	s.Timeout = timeout
263
264	return &s
265}
266
267// SetRetries creates a copy of the session and sets the passed retries into it
268// before returning it.
269func (r *Session) SetRetries(retries int) *Session {
270	var s Session
271	s = *r
272	s.Retries = retries
273
274	return &s
275}
276
277// SetRetryWait creates a copy of the session and sets the passed retryWait into it
278// before returning it.
279func (r *Session) SetRetryWait(retryWait time.Duration) *Session {
280	var s Session
281	s = *r
282	s.RetryWait = retryWait
283
284	return &s
285}
286
287// AppendUserAgent allows higher level application to identify themselves by
288// appending to the useragent string
289func (r *Session) AppendUserAgent(agent string) {
290	if r.userAgent == "" {
291		r.userAgent = getDefaultUserAgent()
292	}
293
294	if agent != "" {
295		r.userAgent += " " + agent
296	}
297}
298
299// ResetUserAgent resets the current user agent to the default value
300func (r *Session) ResetUserAgent() {
301	r.userAgent = getDefaultUserAgent()
302}
303
304func envFallback(keyName string, value *string) {
305	if *value == "" {
306		*value = os.Getenv(keyName)
307	}
308}
309
310func getDefaultTransport(endpointURL string) TransportHandler {
311	var transportHandler TransportHandler
312
313	if strings.Contains(endpointURL, "/xmlrpc/") {
314		transportHandler = &XmlRpcTransport{}
315	} else {
316		transportHandler = &RestTransport{}
317	}
318
319	return transportHandler
320}
321
322func isTimeout(err error) bool {
323	if slErr, ok := err.(sl.Error); ok {
324		switch slErr.StatusCode {
325		case 408, 504, 599:
326			return true
327		}
328	}
329
330	if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
331		return true
332	}
333
334	if netErr, ok := err.(*net.OpError); ok && netErr.Timeout() {
335		return true
336	}
337
338	if netErr, ok := err.(net.UnknownNetworkError); ok && netErr.Timeout() {
339		return true
340	}
341
342	return false
343}
344
345func hasRetryableCode(err error) bool {
346	for _, code := range retryableErrorCodes {
347		if slErr, ok := err.(sl.Error); ok {
348			if slErr.Exception == code {
349				return true
350			}
351		}
352	}
353	return false
354}
355
356func isRetryable(err error) bool {
357	return isTimeout(err) || hasRetryableCode(err)
358}
359
360func getDefaultUserAgent() string {
361	return fmt.Sprintf("softlayer-go/%s (%s;%s;%s)", sl.Version.String(),
362		runtime.Version(),
363		runtime.GOARCH,
364		runtime.GOOS,
365	)
366}
367