client.go raw

   1  // Copyright 2018 Adam S Levy
   2  //
   3  // Permission is hereby granted, free of charge, to any person obtaining a copy
   4  // of this software and associated documentation files (the "Software"), to
   5  // deal in the Software without restriction, including without limitation the
   6  // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
   7  // sell copies of the Software, and to permit persons to whom the Software is
   8  // furnished to do so, subject to the following conditions:
   9  //
  10  // The above copyright notice and this permission notice shall be included in
  11  // all copies or substantial portions of the Software.
  12  //
  13  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  14  // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  15  // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  16  // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  17  // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
  18  // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
  19  // IN THE SOFTWARE.
  20  
  21  package jsonrpc2
  22  
  23  import (
  24  	"bytes"
  25  	"context"
  26  	"encoding/json"
  27  	"fmt"
  28  	"io/ioutil"
  29  	"log"
  30  	"math/rand"
  31  	"net/http"
  32  	"os"
  33  )
  34  
  35  // Logger allows custom log types to be used with the Client when
  36  // Client.DebugRequest is true.
  37  type Logger interface {
  38  	Println(...interface{})
  39  	Printf(string, ...interface{})
  40  }
  41  
  42  func newErrorUnexpectedHTTPResponse(err error, body []byte, res *http.Response) error {
  43  	return ErrorUnexpectedHTTPResponse{err, body, res}
  44  }
  45  
  46  // ErrorUnexpectedHTTPResponse wraps errors that occur during unmarshling of
  47  // the http.Response.Body into a Response, along with the full bytes of the
  48  // http.Response.Body and the http.Response itself.
  49  type ErrorUnexpectedHTTPResponse struct {
  50  	UnmarshlingErr error
  51  	Body           []byte
  52  	*http.Response
  53  }
  54  
  55  // Error returns err.UnmarshalingErr.Error().
  56  func (err ErrorUnexpectedHTTPResponse) Error() string {
  57  	return err.UnmarshlingErr.Error()
  58  }
  59  
  60  // Unwrap return err.UnmarshalingErr.
  61  func (err *ErrorUnexpectedHTTPResponse) Unwrap() error {
  62  	return err.UnmarshlingErr
  63  }
  64  
  65  // Client embeds http.Client and provides a convenient way to make JSON-RPC 2.0
  66  // requests.
  67  type Client struct {
  68  	http.Client
  69  	DebugRequest bool
  70  	Log          Logger
  71  
  72  	BasicAuth bool
  73  	User      string
  74  	Password  string
  75  	Header    http.Header
  76  }
  77  
  78  // Request uses c to make a JSON-RPC 2.0 Request to url with the given method
  79  // and params, and then parses the Response using the provided result, which
  80  // should be a pointer so that it may be populated.
  81  //
  82  // If ctx is not nil, it is added to the http.Request.
  83  //
  84  // If the http.Response is received without error, but cannot be parsed into a
  85  // Response, then an ErrorUnexpectedHTTPResponse is returned containing the
  86  // Unmarshaling error, the raw bytes of the http.Response.Body, and the
  87  // http.Response.
  88  //
  89  // If the Response.HasError() is true, then the Error is returned.
  90  //
  91  // Other potential errors can result from json.Marshal and params,
  92  // http.NewRequest and url, or network errors from c.Do.
  93  //
  94  // A pseudorandom uint between 1 and 5000 is used for the Request.ID.
  95  //
  96  // The "Content-Type":"application/json" header is added to the http.Request,
  97  // and then headers in c.Header are added, which may override the
  98  // "Content-Type".
  99  //
 100  // If c.BasicAuth is true then http.Request.SetBasicAuth(c.User, c.Password) is
 101  // be called.
 102  //
 103  // If c.DebugRequest is true then the Request and Response are printed using
 104  // c.Log. If c.Log == nil, then c.Log = log.New(os.Stderr, "", 0).
 105  func (c *Client) Request(ctx context.Context, url, method string,
 106  	params, result interface{}) error {
 107  
 108  	// Generate a psuedo random ID for this request.
 109  	reqID := rand.Int()%5000 + 1
 110  
 111  	// Marshal the JSON RPC Request.
 112  	req := Request{ID: reqID, Method: method, Params: params}
 113  	if c.DebugRequest {
 114  		if c.Log == nil {
 115  			c.Log = log.New(os.Stderr, "", 0)
 116  		}
 117  		c.Log.Println(req)
 118  	}
 119  	reqData, err := req.MarshalJSON()
 120  	if err != nil {
 121  		return err
 122  	}
 123  
 124  	// Compose the HTTP request.
 125  	httpReq, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(reqData))
 126  	if err != nil {
 127  		return err
 128  	}
 129  	if ctx != nil {
 130  		httpReq = httpReq.WithContext(ctx)
 131  	}
 132  	httpReq.Header.Add(http.CanonicalHeaderKey("Content-Type"), "application/json")
 133  	for k, v := range c.Header {
 134  		httpReq.Header[http.CanonicalHeaderKey(k)] = v
 135  	}
 136  	if c.BasicAuth {
 137  		httpReq.SetBasicAuth(c.User, c.Password)
 138  	}
 139  
 140  	// Make the request.
 141  	httpRes, err := c.Do(httpReq)
 142  	if err != nil {
 143  		return err
 144  	}
 145  	defer httpRes.Body.Close()
 146  
 147  	// Read the HTTP response.
 148  	body, err := ioutil.ReadAll(httpRes.Body)
 149  	if err != nil {
 150  		return err
 151  	}
 152  	if c.DebugRequest {
 153  		fmt.Println("<--", string(body))
 154  		fmt.Println()
 155  	}
 156  
 157  	// Unmarshal the HTTP response into a JSON RPC response.
 158  	var resID int
 159  	res := Response{Result: result, ID: &resID}
 160  	if err := json.Unmarshal(body, &res); err != nil {
 161  		return newErrorUnexpectedHTTPResponse(err, body, httpRes)
 162  	}
 163  
 164  	if res.HasError() {
 165  		return res.Error
 166  	}
 167  
 168  	return nil
 169  }
 170