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 }