session.go raw

   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