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 //go:generate go run github.com/maxbrunsfeld/counterfeiter/v6 -generate
17 18 package session
19 20 import (
21 "context"
22 "encoding/base64"
23 "encoding/json"
24 "fmt"
25 "io/ioutil"
26 "log"
27 "math/rand"
28 "net"
29 "net/http"
30 "net/url"
31 "os"
32 "os/user"
33 "strings"
34 "time"
35 36 "github.com/softlayer/softlayer-go/config"
37 "github.com/softlayer/softlayer-go/sl"
38 )
39 40 // Logger is the logger used by the SoftLayer session package. Can be overridden by the user.
41 var Logger *log.Logger
42 43 func init() {
44 // initialize the logger used by the session package.
45 Logger = log.New(os.Stderr, "", log.LstdFlags)
46 }
47 48 // DefaultEndpoint is the default endpoint for API calls, when no override is provided.
49 const DefaultEndpoint = "https://api.softlayer.com/rest/v3.1"
50 51 const IBMCLOUDIAMENDPOINT = "https://iam.cloud.ibm.com/identity/token"
52 53 // IAMTokenResponse ...
54 type IAMTokenResponse struct {
55 AccessToken string `json:"access_token"`
56 RefreshToken string `json:"refresh_token"`
57 TokenType string `json:"token_type"`
58 }
59 60 // IAMErrorMessage -
61 type IAMErrorMessage struct {
62 ErrorMessage string `json:"errormessage"`
63 ErrorCode string `json:"errorcode"`
64 }
65 66 var retryableErrorCodes = []string{"SoftLayer_Exception_WebService_RateLimitExceeded"}
67 68 // TransportHandler interface for the protocol-specific handling of API requests.
69 //
70 //counterfeiter:generate . TransportHandler
71 type TransportHandler interface {
72 // DoRequest is the protocol-specific handler for making API requests.
73 //
74 // sess is a reference to the current session object, where authentication and
75 // endpoint information can be found.
76 //
77 // service and method are the SoftLayer service name and method name, exactly as they
78 // are documented at http://sldn.softlayer.com/reference/softlayerapi (i.e., with the
79 // 'SoftLayer_' prefix and properly cased.
80 //
81 // args is a slice of arguments required for the service method being invoked. The
82 // types of each argument varies. See the method definition in the services package
83 // for the expected type of each argument.
84 //
85 // options is an sl.Options struct, containing any mask, filter, or result limit values
86 // to be applied.
87 //
88 // pResult is a pointer to a variable to be populated with the result of the API call.
89 // DoRequest should ensure that the native API response (i.e., XML or JSON) is correctly
90 // unmarshaled into the result structure.
91 //
92 // A sl.Error is returned, and can be (with a type assertion) inspected for details of
93 // the error (http code, API error message, etc.), or simply handled as a generic error,
94 // (in which case no type assertion would be necessary)
95 DoRequest(
96 sess *Session,
97 service string,
98 method string,
99 args []interface{},
100 options *sl.Options,
101 pResult interface{}) error
102 }
103 104 const (
105 DefaultTimeout = time.Second * 120
106 DefaultRetryWait = time.Second * 3
107 )
108 109 // Session stores the information required for communication with the SoftLayer API
110 111 type Session struct {
112 // UserName is the name of the SoftLayer API user
113 UserName string
114 115 // ApiKey is the secret for making API calls
116 APIKey string
117 118 // Endpoint is the SoftLayer API endpoint to communicate with
119 Endpoint string
120 121 // UserId is the user id for token-based authentication
122 UserId int
123 124 //IAMToken is the IAM token secret that included IMS account for token-based authentication
125 IAMToken string
126 127 //IAMRefreshToken is the IAM refresh token secret that required to refresh IAM Token
128 IAMRefreshToken string
129 130 // A list objects that implement the IAMUpdater interface.
131 // When a IAMToken is refreshed, these are notified with the new token and new refresh token.
132 IAMUpdaters []IAMUpdater
133 134 // AuthToken is the token secret for token-based authentication
135 AuthToken string
136 137 // Debug controls logging of request details (URI, parameters, etc.)
138 Debug bool
139 140 // The handler whose DoRequest() function will be called for each API request.
141 // Handles the request and any response parsing specific to the desired protocol
142 // (e.g., REST). Set automatically for a new Session, based on the
143 // provided Endpoint.
144 TransportHandler TransportHandler
145 146 // HTTPClient This allows a custom user configured HTTP Client.
147 HTTPClient *http.Client
148 149 // Context allows a custom context.Context for outbound HTTP requests
150 Context context.Context
151 152 // Custom Headers to be used on each request (Currently only for rest)
153 Headers map[string]string
154 155 // Timeout specifies a time limit for http requests made by this
156 // session. Requests that take longer that the specified timeout
157 // will result in an error.
158 Timeout time.Duration
159 160 // Retries is the number of times to retry a connection that failed due to a timeout.
161 Retries int
162 163 // RetryWait minimum wait time to retry a request
164 RetryWait time.Duration
165 166 // userAgent is the user agent to send with each API request
167 // User shouldn't be able to change or set the base user agent
168 userAgent string
169 170 // Last API call made in a human readable format
171 LastCall string
172 }
173 174 //counterfeiter:generate . SLSession
175 type SLSession interface {
176 DoRequest(service string, method string, args []interface{}, options *sl.Options, pResult interface{}) error
177 SetTimeout(timeout time.Duration) *Session
178 SetRetries(retries int) *Session
179 SetRetryWait(retryWait time.Duration) *Session
180 AppendUserAgent(agent string)
181 ResetUserAgent()
182 String() string
183 }
184 185 func init() {
186 rand.Seed(time.Now().UnixNano())
187 }
188 189 // New creates and returns a pointer to a new session object. It takes up to
190 // four parameters, all of which are optional. If specified, they will be
191 // interpreted in the following sequence:
192 //
193 // 1. UserName
194 // 2. Api Key
195 // 3. Endpoint
196 // 4. Timeout
197 //
198 // If one or more are omitted, New() will attempt to retrieve these values from
199 // the environment, and the ~/.softlayer config file, in that order.
200 func New(args ...interface{}) *Session {
201 keys := map[string]int{"username": 0, "api_key": 1, "endpoint_url": 2, "timeout": 3}
202 values := []string{"", "", "", ""}
203 204 for i := 0; i < len(args); i++ {
205 values[i] = args[i].(string)
206 }
207 208 // Default to the environment variables
209 210 // Prioritize SL_USERNAME
211 envFallback("SL_USERNAME", &values[keys["username"]])
212 envFallback("SOFTLAYER_USERNAME", &values[keys["username"]])
213 214 // Prioritize SL_API_KEY
215 envFallback("SL_API_KEY", &values[keys["api_key"]])
216 envFallback("SOFTLAYER_API_KEY", &values[keys["api_key"]])
217 218 // Prioritize SL_ENDPOINT_URL
219 envFallback("SL_ENDPOINT_URL", &values[keys["endpoint_url"]])
220 envFallback("SOFTLAYER_ENDPOINT_URL", &values[keys["endpoint_url"]])
221 222 envFallback("SL_TIMEOUT", &values[keys["timeout"]])
223 envFallback("SOFTLAYER_TIMEOUT", &values[keys["timeout"]])
224 225 // Read ~/.softlayer for configuration
226 var homeDir string
227 u, err := user.Current()
228 if err != nil {
229 for _, name := range []string{"HOME", "USERPROFILE"} { // *nix, windows
230 if dir := os.Getenv(name); dir != "" {
231 homeDir = dir
232 break
233 }
234 }
235 } else {
236 homeDir = u.HomeDir
237 }
238 239 if homeDir != "" {
240 configPath := fmt.Sprintf("%s/.softlayer", homeDir)
241 if _, err = os.Stat(configPath); !os.IsNotExist(err) {
242 // config file exists
243 file, err := config.LoadFile(configPath)
244 if err != nil {
245 log.Println(fmt.Sprintf("[WARN] session: Could not parse %s : %s", configPath, err))
246 } else {
247 for k, v := range keys {
248 value, ok := file.Get("softlayer", k)
249 if ok && values[v] == "" {
250 values[v] = value
251 }
252 }
253 }
254 }
255 } else {
256 log.Println("[WARN] session: home dir could not be determined. Skipping read of ~/.softlayer.")
257 }
258 259 endpointURL := values[keys["endpoint_url"]]
260 if endpointURL == "" {
261 endpointURL = DefaultEndpoint
262 }
263 264 sess := &Session{
265 UserName: values[keys["username"]],
266 APIKey: values[keys["api_key"]],
267 Endpoint: endpointURL,
268 userAgent: getDefaultUserAgent(),
269 }
270 271 timeout := values[keys["timeout"]]
272 if timeout != "" {
273 timeoutDuration, err := time.ParseDuration(fmt.Sprintf("%ss", timeout))
274 if err == nil {
275 sess.Timeout = timeoutDuration
276 }
277 }
278 279 sess.RetryWait = DefaultRetryWait
280 281 return sess
282 }
283 284 // DoRequest hands off the processing to the assigned transport handler. It is
285 // normally called internally by the service objects, but is exported so that it can
286 // be invoked directly by client code in exceptional cases where direct control is
287 // needed over one of the parameters.
288 //
289 // For a description of parameters, see TransportHandler.DoRequest in this package
290 func (r *Session) DoRequest(service string, method string, args []interface{}, options *sl.Options, pResult interface{}) error {
291 if r.TransportHandler == nil {
292 r.TransportHandler = getDefaultTransport(r.Endpoint)
293 }
294 295 err := r.TransportHandler.DoRequest(r, service, method, args, options, pResult)
296 //Check if this is a refreshable exception and try 1 more time
297 if err != nil && r.IAMRefreshToken != "" && NeedsRefresh(err) {
298 r.RefreshToken()
299 err = r.TransportHandler.DoRequest(r, service, method, args, options, pResult)
300 }
301 r.LastCall = CallToString(service, method, args, options)
302 if err != nil {
303 return err
304 }
305 return err
306 }
307 308 // SetTimeout creates a copy of the session and sets the passed timeout into it
309 // before returning it.
310 func (r *Session) SetTimeout(timeout time.Duration) *Session {
311 var s Session
312 s = *r
313 s.Timeout = timeout
314 315 return &s
316 }
317 318 // SetRetries creates a copy of the session and sets the passed retries into it
319 // before returning it.
320 func (r *Session) SetRetries(retries int) *Session {
321 var s Session
322 s = *r
323 s.Retries = retries
324 325 return &s
326 }
327 328 // SetRetryWait creates a copy of the session and sets the passed retryWait into it
329 // before returning it.
330 func (r *Session) SetRetryWait(retryWait time.Duration) *Session {
331 var s Session
332 s = *r
333 s.RetryWait = retryWait
334 335 return &s
336 }
337 338 // AppendUserAgent allows higher level application to identify themselves by
339 // appending to the useragent string
340 func (r *Session) AppendUserAgent(agent string) {
341 if r.userAgent == "" {
342 r.userAgent = getDefaultUserAgent()
343 }
344 345 if agent != "" {
346 r.userAgent += " " + agent
347 }
348 }
349 350 // ResetUserAgent resets the current user agent to the default value
351 func (r *Session) ResetUserAgent() {
352 r.userAgent = getDefaultUserAgent()
353 }
354 355 // Refreshes an IAM authenticated session
356 func (r *Session) RefreshToken() error {
357 client := http.DefaultClient
358 reqPayload := url.Values{}
359 reqPayload.Add("grant_type", "refresh_token")
360 reqPayload.Add("refresh_token", r.IAMRefreshToken)
361 362 req, err := http.NewRequest("POST", IBMCLOUDIAMENDPOINT, strings.NewReader(reqPayload.Encode()))
363 364 if err != nil {
365 return err
366 }
367 req.Header.Add("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte("bx:bx")))
368 req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
369 req.Header.Add("Accept", "application/json")
370 var token IAMTokenResponse
371 var eresp IAMErrorMessage
372 373 resp, err := client.Do(req)
374 if err != nil {
375 return err
376 }
377 378 defer resp.Body.Close()
379 380 responseBody, err := ioutil.ReadAll(resp.Body)
381 382 if err != nil {
383 return err
384 }
385 386 if resp != nil && resp.StatusCode != 200 {
387 err = json.Unmarshal(responseBody, &eresp)
388 if err != nil {
389 return err
390 }
391 if eresp.ErrorCode != "" {
392 return sl.Error{Exception: eresp.ErrorCode, Message: eresp.ErrorMessage}
393 }
394 }
395 396 err = json.Unmarshal(responseBody, &token)
397 if err != nil {
398 return err
399 }
400 401 r.IAMToken = fmt.Sprintf("%s %s", token.TokenType, token.AccessToken)
402 r.IAMRefreshToken = token.RefreshToken
403 // Mostly these are needed if we want to save these new tokens to a config file.
404 for _, updater := range r.IAMUpdaters {
405 updater.Update(r.IAMToken, r.IAMRefreshToken)
406 }
407 return nil
408 }
409 410 // Returns a string of the last api call made.
411 func (r *Session) String() string {
412 return r.LastCall
413 }
414 415 // Adds a new IAMUpdater instance to the session
416 // Useful if you want to update a config file with the new Tokens
417 func (r *Session) AddIAMUpdater(updater IAMUpdater) {
418 r.IAMUpdaters = append(r.IAMUpdaters, updater)
419 }
420 421 func envFallback(keyName string, value *string) {
422 if *value == "" {
423 *value = os.Getenv(keyName)
424 }
425 }
426 427 func getDefaultTransport(endpointURL string) TransportHandler {
428 var transportHandler TransportHandler
429 430 if strings.Contains(endpointURL, "/xmlrpc/") {
431 transportHandler = &XmlRpcTransport{}
432 } else {
433 transportHandler = &RestTransport{}
434 }
435 436 return transportHandler
437 }
438 439 func isTimeout(err error) bool {
440 if slErr, ok := err.(sl.Error); ok {
441 switch slErr.StatusCode {
442 case 408, 504, 599:
443 return true
444 }
445 }
446 447 if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
448 return true
449 }
450 451 if netErr, ok := err.(*net.OpError); ok && netErr.Timeout() {
452 return true
453 }
454 455 if netErr, ok := err.(net.UnknownNetworkError); ok && netErr.Timeout() {
456 return true
457 }
458 459 return false
460 }
461 462 func hasRetryableCode(err error) bool {
463 for _, code := range retryableErrorCodes {
464 if slErr, ok := err.(sl.Error); ok {
465 if slErr.Exception == code {
466 return true
467 }
468 }
469 }
470 return false
471 }
472 473 func isRetryable(err error) bool {
474 return isTimeout(err) || hasRetryableCode(err)
475 }
476 477 // Detects if the SL API returned a specific exception indicating the IAMToken is expired.
478 func NeedsRefresh(err error) bool {
479 if slError, ok := err.(sl.Error); ok {
480 if slError.StatusCode == 500 && slError.Exception == "SoftLayer_Exception_Account_Authentication_AccessTokenValidation" {
481 return true
482 }
483 }
484 return false
485 }
486 487 // Set ENV Variable SL_USERAGENT to append that to the useragent string
488 func getDefaultUserAgent() string {
489 envAgent := os.Getenv("SL_USERAGENT")
490 if envAgent != "" {
491 envAgent = fmt.Sprintf("(%s)", envAgent)
492 }
493 return fmt.Sprintf("softlayer-go/%s %s ", sl.Version.String(), envAgent)
494 }
495 496 // Formats an API call into a readable string
497 func CallToString(service string, method string, args []interface{}, options *sl.Options) string {
498 if options == nil {
499 options = new(sl.Options)
500 }
501 default_id := 0
502 default_mask := "''"
503 default_filter := "''"
504 default_args := ""
505 if options.Id != nil {
506 default_id = *options.Id
507 }
508 if options.Mask != "" {
509 default_mask = fmt.Sprintf(`'%s'`, options.Mask)
510 }
511 if options.Filter != "" {
512 default_filter = fmt.Sprintf(`'%s'`, options.Filter)
513 }
514 if len(args) > 0 {
515 // This is what softlayer-go/session/rest.go does
516 parameters, err := json.Marshal(map[string]interface{}{"parameters": args})
517 default_args = fmt.Sprintf(`'%s'`, string(parameters))
518 if err != nil {
519 default_args = err.Error()
520 }
521 522 }
523 return fmt.Sprintf(
524 "%s::%s(id=%d, mask=%s, filter=%s, %s)",
525 service, method, default_id, default_mask, default_filter, default_args,
526 )
527 }
528