rest.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  
  17  package session
  18  
  19  import (
  20  	"bytes"
  21  	"encoding/json"
  22  	"fmt"
  23  	"io/ioutil"
  24  	"math/rand"
  25  	"net/http"
  26  	"net/url"
  27  	"reflect"
  28  	"strconv"
  29  	"strings"
  30  	"time"
  31  
  32  	"github.com/softlayer/softlayer-go/datatypes"
  33  	"github.com/softlayer/softlayer-go/sl"
  34  )
  35  
  36  type RestTransport struct{}
  37  
  38  // DoRequest - Implementation of the TransportHandler interface for handling
  39  // calls to the REST endpoint.
  40  func (r *RestTransport) DoRequest(sess *Session, service string, method string, args []interface{}, options *sl.Options, pResult interface{}) error {
  41  	restMethod := httpMethod(method, args)
  42  
  43  	// Parse any method parameters and determine the HTTP method
  44  	var parameters []byte
  45  	if len(args) > 0 {
  46  		// parse the parameters
  47  		parameters, _ = json.Marshal(
  48  			map[string]interface{}{
  49  				"parameters": args,
  50  			})
  51  	}
  52  
  53  	path := buildPath(service, method, options)
  54  
  55  	resp, code, err := sendHTTPRequest(sess, path, restMethod, parameters, options)
  56  
  57  	if err != nil {
  58  		//Preserve the original sl error
  59  		if _, ok := err.(sl.Error); ok {
  60  			return err
  61  		}
  62  		return sl.Error{Wrapped: err, StatusCode: code}
  63  	}
  64  
  65  	err = findResponseError(code, resp)
  66  	if err != nil {
  67  		return err
  68  	}
  69  
  70  	// Some APIs that normally return a collection, omit the []'s when the API returns a single value
  71  	returnType := reflect.TypeOf(pResult).String()
  72  	if strings.Index(returnType, "[]") == 1 && strings.Index(string(resp), "[") != 0 {
  73  		resp = []byte("[" + string(resp) + "]")
  74  	}
  75  
  76  	// At this point, all that's left to do is parse the return value to the appropriate type, and return
  77  	// any parse errors (or nil if successful)
  78  
  79  	err = nil
  80  	switch pResult.(type) {
  81  	case *[]uint8:
  82  		// exclude quotes
  83  		*pResult.(*[]uint8) = resp[1 : len(resp)-1]
  84  	case *datatypes.Void:
  85  	case *uint:
  86  		var val uint64
  87  		val, err = strconv.ParseUint(string(resp), 0, 64)
  88  		if err == nil {
  89  			*pResult.(*uint) = uint(val)
  90  		}
  91  	case *bool:
  92  		*pResult.(*bool), err = strconv.ParseBool(string(resp))
  93  	case *string:
  94  		str := string(resp)
  95  		strIdx := len(str) - 1
  96  		if str == "null" {
  97  			str = ""
  98  		} else if str[0] == '"' && str[strIdx] == '"' {
  99  			rawStr := rawString{str}
 100  			err = json.Unmarshal([]byte(`{"val":`+str+`}`), &rawStr)
 101  			if err == nil {
 102  				str = rawStr.Val
 103  			}
 104  		}
 105  		*pResult.(*string) = str
 106  	default:
 107  		// Must be a json representation of one of the many softlayer datatypes
 108  		err = json.Unmarshal(resp, pResult)
 109  	}
 110  
 111  	if err != nil {
 112  		err = sl.Error{Message: err.Error(), Wrapped: err}
 113  	}
 114  
 115  	return err
 116  }
 117  
 118  type rawString struct {
 119  	Val string
 120  }
 121  
 122  func buildPath(service string, method string, options *sl.Options) string {
 123  	path := service
 124  
 125  	if options.Id != nil {
 126  		path = path + "/" + strconv.Itoa(*options.Id)
 127  	}
 128  
 129  	// omit the API method name if the method represents one of the basic REST methods
 130  	if method != "getObject" && method != "deleteObject" && method != "createObject" &&
 131  		method != "editObject" && method != "editObjects" {
 132  		path = path + "/" + method
 133  	}
 134  
 135  	return path + ".json"
 136  }
 137  
 138  func encodeQuery(opts *sl.Options) string {
 139  	query := new(url.URL).Query()
 140  
 141  	if opts.Mask != "" {
 142  		query.Add("objectMask", opts.Mask)
 143  	}
 144  
 145  	if opts.Filter != "" {
 146  		query.Add("objectFilter", opts.Filter)
 147  	}
 148  
 149  	// resultLimit=<offset>,<limit>
 150  	// If offset unspecified, default to 0
 151  	if opts.Limit != nil {
 152  		startOffset := 0
 153  		if opts.Offset != nil {
 154  			startOffset = *opts.Offset
 155  		}
 156  
 157  		query.Add("resultLimit", fmt.Sprintf("%d,%d", startOffset, *opts.Limit))
 158  	}
 159  
 160  	return query.Encode()
 161  }
 162  
 163  func sendHTTPRequest(
 164  	sess *Session, path string, requestType string,
 165  	requestBody []byte, options *sl.Options) ([]byte, int, error) {
 166  
 167  	retries := sess.Retries
 168  	if retries < 2 {
 169  		return makeHTTPRequest(sess, path, requestType, requestBody, options)
 170  	}
 171  
 172  	wait := sess.RetryWait
 173  	if wait == 0 {
 174  		wait = DefaultRetryWait
 175  	}
 176  
 177  	return tryHTTPRequest(retries, wait, sess, path, requestType, requestBody, options)
 178  }
 179  
 180  func tryHTTPRequest(
 181  	retries int, wait time.Duration, sess *Session,
 182  	path string, requestType string, requestBody []byte,
 183  	options *sl.Options) ([]byte, int, error) {
 184  
 185  	resp, code, err := makeHTTPRequest(sess, path, requestType, requestBody, options)
 186  	if err != nil {
 187  		if !isRetryable(err) {
 188  			return resp, code, err
 189  		}
 190  
 191  		if retries--; retries > 0 {
 192  			jitter := time.Duration(rand.Int63n(int64(wait)))
 193  			wait = wait + jitter/2
 194  			time.Sleep(wait)
 195  			return tryHTTPRequest(
 196  				retries, wait, sess, path, requestType, requestBody, options)
 197  		}
 198  	}
 199  
 200  	return resp, code, err
 201  }
 202  
 203  func makeHTTPRequest(
 204  	session *Session, path string, requestType string,
 205  	requestBody []byte, options *sl.Options) ([]byte, int, error) {
 206  	log := Logger
 207  
 208  	client := session.HTTPClient
 209  	if client == nil {
 210  		client = &http.Client{}
 211  	}
 212  
 213  	client.Timeout = DefaultTimeout
 214  	if session.Timeout != 0 {
 215  		client.Timeout = session.Timeout
 216  	}
 217  
 218  	var url string
 219  	if session.Endpoint == "" {
 220  		url = url + DefaultEndpoint
 221  	} else {
 222  		url = url + session.Endpoint
 223  	}
 224  	// fmt.Printf("Calling %s/%s", strings.TrimRight(url, "/"), path)
 225  	url = fmt.Sprintf("%s/%s", strings.TrimRight(url, "/"), path)
 226  	req, err := http.NewRequest(requestType, url, bytes.NewBuffer(requestBody))
 227  	if err != nil {
 228  		return nil, 0, err
 229  	}
 230  
 231  	switch {
 232  	case session.APIKey != "":
 233  		req.SetBasicAuth(session.UserName, session.APIKey)
 234  	case session.AuthToken != "":
 235  		req.SetBasicAuth(fmt.Sprintf("%d", session.UserId), session.AuthToken)
 236  	case session.IAMToken != "":
 237  		req.Header.Set("Authorization", session.IAMToken)
 238  	}
 239  
 240  	// For cases where session is built from the raw structure and not using New() , the UserAgent would be empty
 241  	if session.userAgent == "" {
 242  		session.userAgent = getDefaultUserAgent()
 243  	}
 244  
 245  	req.Header.Set("User-Agent", session.userAgent)
 246  
 247  	if session.Headers != nil {
 248  		for key, value := range session.Headers {
 249  			req.Header.Set(key, value)
 250  		}
 251  	}
 252  
 253  	req.URL.RawQuery = encodeQuery(options)
 254  
 255  	if session.Debug {
 256  		log.Println("[DEBUG] Request URL: ", requestType, req.URL)
 257  		log.Println("[DEBUG] Parameters: ", string(requestBody))
 258  	}
 259  
 260  	// Apply custom context.Context, if supplied
 261  	if session.Context != nil {
 262  		req = req.WithContext(session.Context)
 263  	}
 264  
 265  	resp, err := client.Do(req)
 266  	if err != nil {
 267  		statusCode := 520
 268  		if resp != nil && resp.StatusCode != 0 {
 269  			statusCode = resp.StatusCode
 270  		}
 271  
 272  		if isTimeout(err) {
 273  			statusCode = 599
 274  		}
 275  
 276  		return nil, statusCode, err
 277  	}
 278  
 279  	defer resp.Body.Close()
 280  
 281  	responseBody, err := ioutil.ReadAll(resp.Body)
 282  
 283  	if err != nil {
 284  		return nil, resp.StatusCode, err
 285  	}
 286  	if resp.Header["Softlayer-Total-Items"] != nil && len(resp.Header["Softlayer-Total-Items"]) == 1 {
 287  		var str_err error
 288  		var total_items int
 289  		total_items, str_err = strconv.Atoi(resp.Header["Softlayer-Total-Items"][0])
 290  		if str_err != nil {
 291  			log.Println("[Error] Unable to convert Softlayer-Total-Items to int: ", str_err)
 292  		}
 293  		options.SetTotalItems(total_items)
 294  	}
 295  
 296  	if session.Debug {
 297  		log.Println("[DEBUG] Status Code: ", resp.StatusCode)
 298  		log.Println("[DEBUG] Response: ", string(responseBody))
 299  	}
 300  	err = findResponseError(resp.StatusCode, responseBody)
 301  	return responseBody, resp.StatusCode, err
 302  }
 303  
 304  func httpMethod(name string, args []interface{}) string {
 305  	if name == "deleteObject" {
 306  		return "DELETE"
 307  	} else if name == "editObject" || name == "editObjects" {
 308  		return "PUT"
 309  	} else if name == "createObject" || name == "createObjects" || len(args) > 0 {
 310  		return "POST"
 311  	}
 312  
 313  	return "GET"
 314  }
 315  
 316  func findResponseError(code int, resp []byte) error {
 317  	if code < 200 || code > 299 {
 318  		e := sl.Error{StatusCode: code}
 319  		err := json.Unmarshal(resp, &e)
 320  		// If unparseable, wrap the json error
 321  		if err != nil {
 322  			e.Wrapped = err
 323  			e.Message = err.Error()
 324  		}
 325  		return e
 326  	}
 327  	// Edge case error detection
 328  	if len(resp) == 0 {
 329  		return sl.Error{
 330  			StatusCode: code,
 331  			Exception:  "SoftLayer_Exception_Public",
 332  			Message:    "Empty Response",
 333  		}
 334  	}
 335  	errorString := `{"error":"Internal Error","code":"SoftLayer_Exception_Public"}`
 336  	if errorString == string(resp) {
 337  		return sl.Error{
 338  			StatusCode: code,
 339  			Exception:  "SoftLayer_Exception_Public",
 340  			Message:    "Internal Error",
 341  		}
 342  	}
 343  	return nil
 344  }
 345