github.com/algorand/go-algorand-sdk@v1.24.0/client/v2/common/common.go (about)

     1  package common
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  
     7  	"fmt"
     8  	"io"
     9  	"io/ioutil"
    10  	"net/http"
    11  	"net/url"
    12  
    13  	"github.com/algorand/go-algorand-sdk/encoding/json"
    14  	"github.com/algorand/go-algorand-sdk/encoding/msgpack"
    15  	"github.com/google/go-querystring/query"
    16  )
    17  
    18  // rawRequestPaths is a set of paths where the body should not be urlencoded
    19  var rawRequestPaths = map[string]bool{
    20  	"/v2/transactions":     true,
    21  	"/v2/teal/compile":     true,
    22  	"/v2/teal/disassemble": true,
    23  	"/v2/teal/dryrun":      true,
    24  }
    25  
    26  // Header is a struct for custom headers.
    27  type Header struct {
    28  	Key   string
    29  	Value string
    30  }
    31  
    32  // Client manages the REST interface for a calling user.
    33  type Client struct {
    34  	serverURL url.URL
    35  	apiHeader string
    36  	apiToken  string
    37  	headers   []*Header
    38  }
    39  
    40  // MakeClient is the factory for constructing a Client for a given endpoint.
    41  func MakeClient(address string, apiHeader, apiToken string) (c *Client, err error) {
    42  	url, err := url.Parse(address)
    43  	if err != nil {
    44  		return
    45  	}
    46  
    47  	c = &Client{
    48  		serverURL: *url,
    49  		apiHeader: apiHeader,
    50  		apiToken:  apiToken,
    51  	}
    52  	return
    53  }
    54  
    55  // MakeClientWithHeaders is the factory for constructing a Client for a given endpoint with additional user defined headers.
    56  func MakeClientWithHeaders(address string, apiHeader, apiToken string, headers []*Header) (c *Client, err error) {
    57  	c, err = MakeClient(address, apiHeader, apiToken)
    58  	if err != nil {
    59  		return
    60  	}
    61  
    62  	c.headers = append(c.headers, headers...)
    63  
    64  	return
    65  }
    66  
    67  type BadRequest error
    68  type InvalidToken error
    69  type NotFound error
    70  type InternalError error
    71  
    72  // extractError checks if the response signifies an error.
    73  // If so, it returns the error.
    74  // Otherwise, it returns nil.
    75  func extractError(code int, errorBuf []byte) error {
    76  	if code == 200 {
    77  		return nil
    78  	}
    79  
    80  	wrappedError := fmt.Errorf("HTTP %v: %s", code, errorBuf)
    81  	switch code {
    82  	case 400:
    83  		return BadRequest(wrappedError)
    84  	case 401:
    85  		return InvalidToken(wrappedError)
    86  	case 404:
    87  		return NotFound(wrappedError)
    88  	case 500:
    89  		return InternalError(wrappedError)
    90  	default:
    91  		return wrappedError
    92  	}
    93  }
    94  
    95  // mergeRawQueries merges two raw queries, appending an "&" if both are non-empty
    96  func mergeRawQueries(q1, q2 string) string {
    97  	if q1 == "" {
    98  		return q2
    99  	}
   100  	if q2 == "" {
   101  		return q1
   102  	}
   103  	return q1 + "&" + q2
   104  }
   105  
   106  // submitFormRaw is a helper used for submitting (ex.) GETs and POSTs to the server
   107  func (client *Client) submitFormRaw(ctx context.Context, path string, params interface{}, requestMethod string, encodeJSON bool, headers []*Header, body interface{}) (resp *http.Response, err error) {
   108  	queryURL := client.serverURL
   109  	queryURL.Path += path
   110  
   111  	var req *http.Request
   112  	var bodyReader io.Reader
   113  	var v url.Values
   114  
   115  	if params != nil {
   116  		v, err = query.Values(params)
   117  		if err != nil {
   118  			return nil, err
   119  		}
   120  	}
   121  
   122  	if requestMethod == "POST" && rawRequestPaths[path] {
   123  		reqBytes, ok := body.([]byte)
   124  		if !ok {
   125  			return nil, fmt.Errorf("couldn't decode raw body as bytes")
   126  		}
   127  		bodyReader = bytes.NewBuffer(reqBytes)
   128  	} else if encodeJSON {
   129  		jsonValue := json.Encode(params)
   130  		bodyReader = bytes.NewBuffer(jsonValue)
   131  	}
   132  
   133  	queryURL.RawQuery = mergeRawQueries(queryURL.RawQuery, v.Encode())
   134  
   135  	req, err = http.NewRequest(requestMethod, queryURL.String(), bodyReader)
   136  	if err != nil {
   137  		return nil, err
   138  	}
   139  
   140  	// Supply the client token.
   141  	req.Header.Set(client.apiHeader, client.apiToken)
   142  	// Add the client headers.
   143  	for _, header := range client.headers {
   144  		req.Header.Add(header.Key, header.Value)
   145  	}
   146  	// Add the request headers.
   147  	for _, header := range headers {
   148  		req.Header.Add(header.Key, header.Value)
   149  	}
   150  
   151  	httpClient := &http.Client{}
   152  	req = req.WithContext(ctx)
   153  	resp, err = httpClient.Do(req)
   154  
   155  	if err != nil {
   156  		select {
   157  		case <-ctx.Done():
   158  			return nil, ctx.Err()
   159  		default:
   160  		}
   161  		return nil, err
   162  	}
   163  	return resp, nil
   164  }
   165  
   166  func (client *Client) submitForm(ctx context.Context, response interface{}, path string, params interface{}, requestMethod string, encodeJSON bool, headers []*Header, body interface{}) error {
   167  	resp, err := client.submitFormRaw(ctx, path, params, requestMethod, encodeJSON, headers, body)
   168  	if err != nil {
   169  		return err
   170  	}
   171  
   172  	defer resp.Body.Close()
   173  	var bodyBytes []byte
   174  	bodyBytes, err = ioutil.ReadAll(resp.Body)
   175  	if err != nil {
   176  		return err
   177  	}
   178  
   179  	responseErr := extractError(resp.StatusCode, bodyBytes)
   180  
   181  	// The caller wants a string
   182  	if strResponse, ok := response.(*string); ok {
   183  		*strResponse = string(bodyBytes)
   184  		return err
   185  	}
   186  
   187  	// Attempt to unmarshal a response regardless of whether or not there was an error.
   188  	err = json.LenientDecode(bodyBytes, response)
   189  	if responseErr != nil {
   190  		// Even if there was an unmarshalling error, return the HTTP error first if there was one.
   191  		return responseErr
   192  	}
   193  	return err
   194  }
   195  
   196  // Get performs a GET request to the specific path against the server
   197  func (client *Client) Get(ctx context.Context, response interface{}, path string, params interface{}, headers []*Header) error {
   198  	return client.submitForm(ctx, response, path, params, "GET", false /* encodeJSON */, headers, nil)
   199  }
   200  
   201  // GetRaw performs a GET request to the specific path against the server and returns the raw body bytes.
   202  func (client *Client) GetRaw(ctx context.Context, path string, params interface{}, headers []*Header) (response []byte, err error) {
   203  	var resp *http.Response
   204  	resp, err = client.submitFormRaw(ctx, path, params, "GET", false /* encodeJSON */, headers, nil)
   205  	if err != nil {
   206  		return nil, err
   207  	}
   208  	defer resp.Body.Close()
   209  	var bodyBytes []byte
   210  	bodyBytes, err = ioutil.ReadAll(resp.Body)
   211  	if err != nil {
   212  		return nil, err
   213  	}
   214  	return bodyBytes, extractError(resp.StatusCode, bodyBytes)
   215  }
   216  
   217  // GetRawMsgpack performs a GET request to the specific path against the server and returns the decoded messagepack response.
   218  func (client *Client) GetRawMsgpack(ctx context.Context, response interface{}, path string, params interface{}, headers []*Header) error {
   219  	resp, err := client.submitFormRaw(ctx, path, params, "GET", false /* encodeJSON */, headers, nil)
   220  	if err != nil {
   221  		return err
   222  	}
   223  
   224  	defer resp.Body.Close()
   225  
   226  	if resp.StatusCode != http.StatusOK {
   227  		var bodyBytes []byte
   228  		bodyBytes, err = ioutil.ReadAll(resp.Body)
   229  		if err != nil {
   230  			return fmt.Errorf("failed to read response body: %+v", err)
   231  		}
   232  
   233  		return extractError(resp.StatusCode, bodyBytes)
   234  	}
   235  
   236  	dec := msgpack.NewLenientDecoder(resp.Body)
   237  	return dec.Decode(&response)
   238  }
   239  
   240  // Post sends a POST request to the given path with the given body object.
   241  // No query parameters will be sent if body is nil.
   242  // response must be a pointer to an object as post writes the response there.
   243  func (client *Client) Post(ctx context.Context, response interface{}, path string, params interface{}, headers []*Header, body interface{}) error {
   244  	return client.submitForm(ctx, response, path, params, "POST", true /* encodeJSON */, headers, body)
   245  }
   246  
   247  // Helper function for correctly formatting and escaping URL path parameters.
   248  // Used in the generated API client code.
   249  func EscapeParams(params ...interface{}) []interface{} {
   250  	paramsStr := make([]interface{}, len(params))
   251  	for i, param := range params {
   252  		switch v := param.(type) {
   253  		case string:
   254  			paramsStr[i] = url.PathEscape(v)
   255  		default:
   256  			paramsStr[i] = fmt.Sprintf("%v", v)
   257  		}
   258  	}
   259  
   260  	return paramsStr
   261  }