| // Copyright (c) 2016 VMware, Inc. All Rights Reserved. |
| // |
| // This product is licensed to you under the Apache License, Version 2.0 (the "License"). |
| // You may not use this product except in compliance with the License. |
| // |
| // This product may include a number of subcomponents with separate copyright notices and |
| // license terms. Your use of these subcomponents is subject to the terms and conditions |
| // of the subcomponent's license, as noted in the LICENSE file. |
| |
| package photon |
| |
| import ( |
| "bytes" |
| "crypto/rand" |
| "encoding/json" |
| "fmt" |
| "io" |
| "io/ioutil" |
| "log" |
| "net/http" |
| "os" |
| "path/filepath" |
| "strings" |
| ) |
| |
| type restClient struct { |
| httpClient *http.Client |
| logger *log.Logger |
| Auth *AuthAPI |
| UpdateAccessTokenCallback TokenCallback |
| } |
| |
| type request struct { |
| Method string |
| URL string |
| ContentType string |
| Body io.Reader |
| Tokens *TokenOptions |
| } |
| |
| type page struct { |
| Items []interface{} `json:"items"` |
| NextPageLink string `json:"nextPageLink"` |
| PreviousPageLink string `json:"previousPageLink"` |
| } |
| |
| type documentList struct { |
| Items []interface{} |
| } |
| |
| type bodyRewinder func() io.Reader |
| |
| const appJson string = "application/json" |
| |
| // Root URL specifies the API version. |
| const rootUrl string = "/v1" |
| |
| // From https://golang.org/src/mime/multipart/writer.go |
| var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") |
| |
| func (client *restClient) AppendSlice(origSlice []interface{}, dataToAppend []interface{}) []interface{} { |
| origLen := len(origSlice) |
| newLen := origLen + len(dataToAppend) |
| |
| if newLen > cap(origSlice) { |
| newSlice := make([]interface{}, (newLen+1)*2) |
| copy(newSlice, origSlice) |
| origSlice = newSlice |
| } |
| |
| origSlice = origSlice[0:newLen] |
| copy(origSlice[origLen:newLen], dataToAppend) |
| |
| return origSlice |
| } |
| |
| func (client *restClient) Get(url string, tokens *TokenOptions) (res *http.Response, err error) { |
| req := request{"GET", url, "", nil, tokens} |
| res, err = client.SendRequest(&req, nil) |
| return |
| } |
| |
| func (client *restClient) GetList(endpoint string, url string, tokens *TokenOptions) (result []byte, err error) { |
| req := request{"GET", url, "", nil, tokens} |
| res, err := client.SendRequest(&req, nil) |
| if err != nil { |
| return |
| } |
| res, err = getError(res) |
| if err != nil { |
| return |
| } |
| |
| decoder := json.NewDecoder(res.Body) |
| decoder.UseNumber() |
| |
| page := &page{} |
| err = decoder.Decode(page) |
| if err != nil { |
| return |
| } |
| |
| documentList := &documentList{} |
| documentList.Items = client.AppendSlice(documentList.Items, page.Items) |
| |
| for page.NextPageLink != "" { |
| req = request{"GET", endpoint + page.NextPageLink, "", nil, tokens} |
| res, err = client.SendRequest(&req, nil) |
| if err != nil { |
| return |
| } |
| res, err = getError(res) |
| if err != nil { |
| return |
| } |
| |
| decoder = json.NewDecoder(res.Body) |
| decoder.UseNumber() |
| |
| page.NextPageLink = "" |
| page.PreviousPageLink = "" |
| |
| err = decoder.Decode(page) |
| if err != nil { |
| return |
| } |
| |
| documentList.Items = client.AppendSlice(documentList.Items, page.Items) |
| } |
| |
| result, err = json.Marshal(documentList) |
| |
| return |
| } |
| |
| func (client *restClient) Post(url string, contentType string, body io.ReadSeeker, tokens *TokenOptions) (res *http.Response, err error) { |
| res, err = client.SendRequestCommon("POST", url, contentType, body, tokens) |
| return |
| } |
| |
| func (client *restClient) Patch(url string, contentType string, body io.ReadSeeker, tokens *TokenOptions) (res *http.Response, err error) { |
| res, err = client.SendRequestCommon("PATCH", url, contentType, body, tokens) |
| return |
| } |
| |
| func (client *restClient) Put(url string, contentType string, body io.ReadSeeker, tokens *TokenOptions) (res *http.Response, err error) { |
| res, err = client.SendRequestCommon("PUT", url, contentType, body, tokens) |
| return |
| } |
| |
| func (client *restClient) Delete(url string, tokens *TokenOptions) (res *http.Response, err error) { |
| req := request{"DELETE", url, "", nil, tokens} |
| res, err = client.SendRequest(&req, nil) |
| return |
| } |
| |
| func (client *restClient) SendRequestCommon(method string, url string, contentType string, body io.ReadSeeker, tokens *TokenOptions) (res *http.Response, err error) { |
| if contentType == "" { |
| contentType = appJson |
| } |
| |
| req := request{method, url, contentType, body, tokens} |
| rewinder := func() io.Reader { |
| body.Seek(0, 0) |
| return body |
| } |
| res, err = client.SendRequest(&req, rewinder) |
| return |
| } |
| |
| func (client *restClient) SendRequest(req *request, bodyRewinder bodyRewinder) (res *http.Response, err error) { |
| res, err = client.sendRequestHelper(req) |
| // In most cases, we'll return immediately |
| // If the operation succeeded, but we got a 401 response and if we're using |
| // authentication, then we'll look into the body to see if the token expired |
| if err != nil { |
| return res, err |
| } |
| if res.StatusCode != 401 { |
| // It's not a 401, so the token didn't expire |
| return res, err |
| } |
| if req.Tokens == nil || req.Tokens.AccessToken == "" { |
| // We don't have a token, so we can't renew the token, no need to proceed |
| return res, err |
| } |
| |
| // We're going to look in the body to see if it failed because the token expired |
| // This means we need to read the body, but the functions that call us also |
| // expect to read the body. So we read the body, then create a new reader |
| // so they can read the body as normal. |
| body, err := ioutil.ReadAll(res.Body) |
| if err != nil { |
| return res, err |
| } |
| res.Body = ioutil.NopCloser(bytes.NewReader(body)) |
| |
| // Now see if we had an expired token or not |
| var apiError ApiError |
| err = json.Unmarshal(body, &apiError) |
| if err != nil { |
| return res, err |
| } |
| if apiError.Code != "ExpiredAuthToken" { |
| return res, nil |
| } |
| |
| // We were told that the access token expired, so try to renew it. |
| // Note that this looks recursive because GetTokensByRefreshToken() will |
| // call the /auth API, and therefore SendRequest(). However, it calls |
| // without a token, so we avoid having a loop |
| newTokens, err := client.Auth.GetTokensByRefreshToken(req.Tokens.RefreshToken) |
| if err != nil { |
| return res, err |
| } |
| req.Tokens.AccessToken = newTokens.AccessToken |
| if client.UpdateAccessTokenCallback != nil { |
| client.UpdateAccessTokenCallback(newTokens.AccessToken) |
| } |
| if req.Body != nil && bodyRewinder != nil { |
| req.Body = bodyRewinder() |
| } |
| res, err = client.sendRequestHelper(req) |
| return res, nil |
| } |
| |
| func (client *restClient) sendRequestHelper(req *request) (res *http.Response, err error) { |
| r, err := http.NewRequest(req.Method, req.URL, req.Body) |
| if err != nil { |
| client.logger.Printf("An error occured creating request %s on %s. Error: %s", req.Method, req.URL, err) |
| return |
| } |
| if req.ContentType != "" { |
| r.Header.Add("Content-Type", req.ContentType) |
| } |
| if req.Tokens != nil && req.Tokens.AccessToken != "" { |
| r.Header.Add("Authorization", "Bearer "+req.Tokens.AccessToken) |
| } |
| res, err = client.httpClient.Do(r) |
| if err != nil { |
| client.logger.Printf("An error occured when calling %s on %s. Error: %s", req.Method, req.URL, err) |
| return |
| } |
| |
| client.logger.Printf("[%s] %s - %s %s", res.Header.Get("request-id"), res.Status, req.Method, req.URL) |
| return |
| } |
| |
| func (client *restClient) MultipartUploadFile(url, filePath string, params map[string]string, tokens *TokenOptions) (res *http.Response, err error) { |
| file, err := os.Open(filePath) |
| if err != nil { |
| return |
| } |
| defer file.Close() |
| return client.MultipartUpload(url, file, filepath.Base(filePath), params, tokens) |
| } |
| |
| func (client *restClient) MultipartUpload(url string, reader io.ReadSeeker, filename string, params map[string]string, tokens *TokenOptions) (res *http.Response, err error) { |
| boundary := client.randomBoundary() |
| multiReader, contentType := client.createMultiReader(reader, filename, params, boundary) |
| rewinder := func() io.Reader { |
| reader.Seek(0, 0) |
| multiReader, _ := client.createMultiReader(reader, filename, params, boundary) |
| return multiReader |
| } |
| res, err = client.SendRequest(&request{"POST", url, contentType, multiReader, tokens}, rewinder) |
| |
| return |
| } |
| |
| func (client *restClient) createMultiReader(reader io.ReadSeeker, filename string, params map[string]string, boundary string) (io.Reader, string) { |
| // The mime/multipart package does not support streaming multipart data from disk, |
| // at least not without complicated, problematic goroutines that simultaneously read/write into a buffer. |
| // A much easier approach is to just construct the multipart request by hand, using io.MultiPart to |
| // concatenate the parts of the request together into a single io.Reader. |
| parts := []io.Reader{} |
| |
| // Create a part for each key, val pair in params |
| for k, v := range params { |
| parts = append(parts, client.createFieldPart(k, v, boundary)) |
| } |
| |
| start := fmt.Sprintf("\r\n--%s\r\n", boundary) |
| start += fmt.Sprintf("Content-Disposition: form-data; name=\"file\"; filename=\"%s\"\r\n", quoteEscaper.Replace(filename)) |
| start += fmt.Sprintf("Content-Type: application/octet-stream\r\n\r\n") |
| end := fmt.Sprintf("\r\n--%s--", boundary) |
| |
| // The request will consist of a reader to begin the request, a reader which points |
| // to the file data on disk, and a reader containing the closing boundary of the request. |
| parts = append(parts, strings.NewReader(start), reader, strings.NewReader(end)) |
| |
| contentType := fmt.Sprintf("multipart/form-data; boundary=%s", boundary) |
| |
| return io.MultiReader(parts...), contentType |
| } |
| |
| // From https://golang.org/src/mime/multipart/writer.go |
| func (client *restClient) randomBoundary() string { |
| var buf [30]byte |
| _, err := io.ReadFull(rand.Reader, buf[:]) |
| if err != nil { |
| panic(err) |
| } |
| return fmt.Sprintf("%x", buf[:]) |
| } |
| |
| // Creates a reader that encapsulates a single multipart form part |
| func (client *restClient) createFieldPart(fieldname, value, boundary string) io.Reader { |
| str := fmt.Sprintf("\r\n--%s\r\n", boundary) |
| str += fmt.Sprintf("Content-Disposition: form-data; name=\"%s\"\r\n\r\n", quoteEscaper.Replace(fieldname)) |
| str += value |
| return strings.NewReader(str) |
| } |