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