github.com/qri-io/qri@v0.10.1-0.20220104210721-c771715036cb/lib/http/http.go (about)

     1  package http
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"encoding/json"
     7  	"errors"
     8  	"fmt"
     9  	"io/ioutil"
    10  	"net/http"
    11  	"net/url"
    12  
    13  	golog "github.com/ipfs/go-log"
    14  	ma "github.com/multiformats/go-multiaddr"
    15  	manet "github.com/multiformats/go-multiaddr/net"
    16  	apiutil "github.com/qri-io/qri/api/util"
    17  	"github.com/qri-io/qri/auth/token"
    18  )
    19  
    20  const (
    21  	// JSONMimeType is the JSON content type header value
    22  	JSONMimeType = "application/json"
    23  	// SourceResolver header name
    24  	SourceResolver = "SourceResolver"
    25  )
    26  
    27  var (
    28  	log = golog.Logger("lib")
    29  	// ErrUnsupportedRPC is an error for when running a method that is not supported via HTTP RPC
    30  	ErrUnsupportedRPC = errors.New("method is not supported over RPC")
    31  )
    32  
    33  // Client makes remote procedure calls to a qri node over HTTP
    34  type Client struct {
    35  	Address  string
    36  	Protocol string
    37  }
    38  
    39  // NewClient instantiates a new Client
    40  func NewClient(multiaddrStr string) (*Client, error) {
    41  	maAddr, err := ma.NewMultiaddr(multiaddrStr)
    42  	if err != nil {
    43  		return nil, err
    44  	}
    45  	// we default to the http protocol
    46  	protocol := "http"
    47  	protocols := maAddr.Protocols()
    48  	httpAddr, err := manet.ToNetAddr(maAddr)
    49  	if err != nil {
    50  		return nil, err
    51  	}
    52  	for _, p := range protocols {
    53  		// if https is present in the multiAddr we preffer that over http
    54  		if p.Code == ma.P_HTTPS {
    55  			protocol = "https"
    56  		}
    57  	}
    58  	return &Client{
    59  		Address:  httpAddr.String(),
    60  		Protocol: protocol,
    61  	}, nil
    62  }
    63  
    64  // NewClientWithProtocol instantiates a new Client with either http or https protocols
    65  func NewClientWithProtocol(multiaddrStr, protocol string) (*Client, error) {
    66  	maAddr, err := ma.NewMultiaddr(multiaddrStr)
    67  	if err != nil {
    68  		return nil, err
    69  	}
    70  	httpAddr, err := manet.ToNetAddr(maAddr)
    71  	if err != nil {
    72  		return nil, err
    73  	}
    74  	return &Client{
    75  		Address:  httpAddr.String(),
    76  		Protocol: protocol,
    77  	}, nil
    78  }
    79  
    80  // Call calls API endpoint and passes on parameters, context info
    81  func (c Client) Call(ctx context.Context, apiEndpoint APIEndpoint, source string, params interface{}, result interface{}) error {
    82  	return c.CallMethod(ctx, apiEndpoint, http.MethodPost, source, params, result)
    83  }
    84  
    85  // CallMethod calls API endpoint and passes on parameters, context info and specific HTTP Method
    86  func (c Client) CallMethod(ctx context.Context, apiEndpoint APIEndpoint, httpMethod string, source string, params interface{}, result interface{}) error {
    87  	// TODO(arqu): work out mimeType configuration/override per API endpoint
    88  	mimeType := JSONMimeType
    89  	addr := fmt.Sprintf("%s://%s%s", c.Protocol, c.Address, apiEndpoint)
    90  
    91  	return c.do(ctx, addr, httpMethod, mimeType, source, params, result, false)
    92  }
    93  
    94  // CallRaw calls API endpoint and passes on parameters, context info and returns the []byte result
    95  func (c Client) CallRaw(ctx context.Context, apiEndpoint APIEndpoint, source string, params interface{}, result interface{}) error {
    96  	return c.CallMethodRaw(ctx, apiEndpoint, http.MethodPost, source, params, result)
    97  }
    98  
    99  // CallMethodRaw calls API endpoint and passes on parameters, context info, specific HTTP Method and returns the []byte result
   100  func (c Client) CallMethodRaw(ctx context.Context, apiEndpoint APIEndpoint, httpMethod string, source string, params interface{}, result interface{}) error {
   101  	// TODO(arqu): work out mimeType configuration/override per API endpoint
   102  	mimeType := JSONMimeType
   103  	addr := fmt.Sprintf("%s://%s%s", c.Protocol, c.Address, apiEndpoint)
   104  	// TODO(arqu): inject context values into headers
   105  
   106  	return c.do(ctx, addr, httpMethod, mimeType, source, params, result, true)
   107  }
   108  
   109  func (c Client) do(ctx context.Context, addr string, httpMethod string, mimeType string, source string, params interface{}, result interface{}, raw bool) error {
   110  	var req *http.Request
   111  	var err error
   112  
   113  	log.Debugf("http: %s - %s", httpMethod, addr)
   114  
   115  	if httpMethod == http.MethodGet || httpMethod == http.MethodDelete {
   116  		u, err := url.Parse(addr)
   117  		if err != nil {
   118  			return err
   119  		}
   120  
   121  		if params != nil {
   122  			if pm, ok := params.(map[string]string); ok {
   123  				qvars := u.Query()
   124  				for k, v := range pm {
   125  					qvars.Set(k, v)
   126  				}
   127  				u.RawQuery = qvars.Encode()
   128  			}
   129  		}
   130  		req, err = http.NewRequest(httpMethod, u.String(), nil)
   131  	} else if httpMethod == http.MethodPost || httpMethod == http.MethodPut {
   132  		payload, err := json.Marshal(params)
   133  		if err != nil {
   134  			return err
   135  		}
   136  		req, err = http.NewRequest(httpMethod, addr, bytes.NewReader(payload))
   137  	}
   138  	if err != nil {
   139  		return err
   140  	}
   141  
   142  	req.Header.Set("Content-Type", mimeType)
   143  	req.Header.Set("Accept", mimeType)
   144  
   145  	if source != "" {
   146  		req.Header.Set(SourceResolver, source)
   147  	}
   148  
   149  	req, added := token.AddContextTokenToRequest(ctx, req)
   150  	if !added {
   151  		log.Debugw("No token was set on an http client request. Unauthenticated requests may fail", "httpMethod", httpMethod, "addr", addr)
   152  	}
   153  
   154  	res, err := http.DefaultClient.Do(req)
   155  	if err != nil {
   156  		return err
   157  	}
   158  
   159  	body, err := ioutil.ReadAll(res.Body)
   160  	if err != nil {
   161  		return err
   162  	}
   163  
   164  	if err = c.checkError(res, body, raw); err != nil {
   165  		return err
   166  	}
   167  
   168  	if raw {
   169  		if buf, ok := result.(*bytes.Buffer); ok {
   170  			buf.Write(body)
   171  		} else {
   172  			return fmt.Errorf("Client raw interface is not a byte buffer")
   173  		}
   174  		return nil
   175  	}
   176  
   177  	if result != nil {
   178  		resData := apiutil.Response{
   179  			Data: result,
   180  			Meta: &apiutil.Meta{},
   181  		}
   182  		err = json.Unmarshal(body, &resData)
   183  		if err != nil {
   184  			log.Debugf("Client response err: %s", err.Error())
   185  			return fmt.Errorf("Client response err: %s", err)
   186  		}
   187  	}
   188  	return nil
   189  }
   190  
   191  func (c Client) checkError(res *http.Response, body []byte, raw bool) error {
   192  	metaResponse := struct {
   193  		Meta *apiutil.Meta
   194  	}{
   195  		Meta: &apiutil.Meta{},
   196  	}
   197  	parseErr := json.Unmarshal(body, &metaResponse)
   198  
   199  	if !raw {
   200  		if parseErr != nil {
   201  			log.Debugf("Client response error: %d - %q", res.StatusCode, body)
   202  			return fmt.Errorf("failed parsing response: %q", string(body))
   203  		}
   204  		if metaResponse.Meta == nil {
   205  			log.Debugf("Client response error: %d - %q", res.StatusCode, body)
   206  			return fmt.Errorf("invalid meta response")
   207  		}
   208  	} else if (metaResponse.Meta.Code < 200 || metaResponse.Meta.Code > 299) && metaResponse.Meta.Code != 0 {
   209  		log.Debugf("Client response meta error: %d - %q", metaResponse.Meta.Code, metaResponse.Meta.Error)
   210  		return fmt.Errorf(metaResponse.Meta.Error)
   211  	}
   212  
   213  	if res.StatusCode < 200 || res.StatusCode > 299 {
   214  		log.Debugf("Client response error: %d - %q", res.StatusCode, body)
   215  		return fmt.Errorf(string(body))
   216  	}
   217  	return nil
   218  }