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 }