github.com/franono/tendermint@v0.32.2-0.20200527150959-749313264ce9/rpc/jsonrpc/client/http_json_client.go (about)

     1  package client
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/json"
     6  	"fmt"
     7  	"io/ioutil"
     8  	"net"
     9  	"net/http"
    10  	"net/url"
    11  	"strings"
    12  	"sync"
    13  
    14  	amino "github.com/tendermint/go-amino"
    15  
    16  	types "github.com/franono/tendermint/rpc/jsonrpc/types"
    17  )
    18  
    19  const (
    20  	protoHTTP  = "http"
    21  	protoHTTPS = "https"
    22  	protoWSS   = "wss"
    23  	protoWS    = "ws"
    24  	protoTCP   = "tcp"
    25  )
    26  
    27  //-------------------------------------------------------------
    28  
    29  // Parsed URL structure
    30  type parsedURL struct {
    31  	url.URL
    32  }
    33  
    34  // Parse URL and set defaults
    35  func newParsedURL(remoteAddr string) (*parsedURL, error) {
    36  	u, err := url.Parse(remoteAddr)
    37  	if err != nil {
    38  		return nil, err
    39  	}
    40  
    41  	// default to tcp if nothing specified
    42  	if u.Scheme == "" {
    43  		u.Scheme = protoTCP
    44  	}
    45  
    46  	return &parsedURL{*u}, nil
    47  }
    48  
    49  // Change protocol to HTTP for unknown protocols and TCP protocol - useful for RPC connections
    50  func (u *parsedURL) SetDefaultSchemeHTTP() {
    51  	// protocol to use for http operations, to support both http and https
    52  	switch u.Scheme {
    53  	case protoHTTP, protoHTTPS, protoWS, protoWSS:
    54  		// known protocols not changed
    55  	default:
    56  		// default to http for unknown protocols (ex. tcp)
    57  		u.Scheme = protoHTTP
    58  	}
    59  }
    60  
    61  // Get full address without the protocol - useful for Dialer connections
    62  func (u parsedURL) GetHostWithPath() string {
    63  	// Remove protocol, userinfo and # fragment, assume opaque is empty
    64  	return u.Host + u.EscapedPath()
    65  }
    66  
    67  // Get a trimmed address - useful for WS connections
    68  func (u parsedURL) GetTrimmedHostWithPath() string {
    69  	// replace / with . for http requests (kvstore domain)
    70  	return strings.Replace(u.GetHostWithPath(), "/", ".", -1)
    71  }
    72  
    73  // Get a trimmed address with protocol - useful as address in RPC connections
    74  func (u parsedURL) GetTrimmedURL() string {
    75  	return u.Scheme + "://" + u.GetTrimmedHostWithPath()
    76  }
    77  
    78  //-------------------------------------------------------------
    79  
    80  // HTTPClient is a common interface for JSON-RPC HTTP clients.
    81  type HTTPClient interface {
    82  	// Call calls the given method with the params and returns a result.
    83  	Call(method string, params map[string]interface{}, result interface{}) (interface{}, error)
    84  	// Codec returns an amino codec used.
    85  	Codec() *amino.Codec
    86  	// SetCodec sets an amino codec.
    87  	SetCodec(*amino.Codec)
    88  }
    89  
    90  // Caller implementers can facilitate calling the JSON-RPC endpoint.
    91  type Caller interface {
    92  	Call(method string, params map[string]interface{}, result interface{}) (interface{}, error)
    93  }
    94  
    95  //-------------------------------------------------------------
    96  
    97  // Client is a JSON-RPC client, which sends POST HTTP requests to the
    98  // remote server.
    99  //
   100  // Request values are amino encoded. Response is expected to be amino encoded.
   101  // New amino codec is used if no other codec was set using SetCodec.
   102  //
   103  // Client is safe for concurrent use by multiple goroutines.
   104  type Client struct {
   105  	address  string
   106  	username string
   107  	password string
   108  
   109  	client *http.Client
   110  	cdc    *amino.Codec
   111  
   112  	mtx       sync.Mutex
   113  	nextReqID int
   114  }
   115  
   116  var _ HTTPClient = (*Client)(nil)
   117  
   118  // Both Client and RequestBatch can facilitate calls to the JSON
   119  // RPC endpoint.
   120  var _ Caller = (*Client)(nil)
   121  var _ Caller = (*RequestBatch)(nil)
   122  
   123  // New returns a Client pointed at the given address.
   124  // An error is returned on invalid remote. The function panics when remote is nil.
   125  func New(remote string) (*Client, error) {
   126  	httpClient, err := DefaultHTTPClient(remote)
   127  	if err != nil {
   128  		return nil, err
   129  	}
   130  	return NewWithHTTPClient(remote, httpClient)
   131  }
   132  
   133  // NewWithHTTPClient returns a Client pointed at the given
   134  // address using a custom http client. An error is returned on invalid remote.
   135  // The function panics when remote is nil.
   136  func NewWithHTTPClient(remote string, client *http.Client) (*Client, error) {
   137  	if client == nil {
   138  		panic("nil http.Client provided")
   139  	}
   140  
   141  	parsedURL, err := newParsedURL(remote)
   142  	if err != nil {
   143  		return nil, fmt.Errorf("invalid remote %s: %s", remote, err)
   144  	}
   145  
   146  	parsedURL.SetDefaultSchemeHTTP()
   147  
   148  	address := parsedURL.GetTrimmedURL()
   149  	username := parsedURL.User.Username()
   150  	password, _ := parsedURL.User.Password()
   151  
   152  	rpcClient := &Client{
   153  		address:  address,
   154  		username: username,
   155  		password: password,
   156  		client:   client,
   157  		cdc:      amino.NewCodec(),
   158  	}
   159  
   160  	return rpcClient, nil
   161  }
   162  
   163  // Call issues a POST HTTP request. Requests are JSON encoded. Content-Type:
   164  // text/json.
   165  func (c *Client) Call(method string, params map[string]interface{}, result interface{}) (interface{}, error) {
   166  	id := c.nextRequestID()
   167  
   168  	request, err := types.MapToRequest(c.cdc, id, method, params)
   169  	if err != nil {
   170  		return nil, fmt.Errorf("failed to encode params: %w", err)
   171  	}
   172  
   173  	requestBytes, err := json.Marshal(request)
   174  	if err != nil {
   175  		return nil, fmt.Errorf("failed to marshal request: %w", err)
   176  	}
   177  
   178  	requestBuf := bytes.NewBuffer(requestBytes)
   179  	httpRequest, err := http.NewRequest(http.MethodPost, c.address, requestBuf)
   180  	if err != nil {
   181  		return nil, fmt.Errorf("request failed: %w", err)
   182  	}
   183  	httpRequest.Header.Set("Content-Type", "text/json")
   184  	if c.username != "" || c.password != "" {
   185  		httpRequest.SetBasicAuth(c.username, c.password)
   186  	}
   187  	httpResponse, err := c.client.Do(httpRequest)
   188  	if err != nil {
   189  		return nil, fmt.Errorf("post failed: %w", err)
   190  	}
   191  	defer httpResponse.Body.Close() // nolint: errcheck
   192  
   193  	responseBytes, err := ioutil.ReadAll(httpResponse.Body)
   194  	if err != nil {
   195  		return nil, fmt.Errorf("failed to read response body: %w", err)
   196  	}
   197  
   198  	return unmarshalResponseBytes(c.cdc, responseBytes, id, result)
   199  }
   200  
   201  func (c *Client) Codec() *amino.Codec       { return c.cdc }
   202  func (c *Client) SetCodec(cdc *amino.Codec) { c.cdc = cdc }
   203  
   204  // NewRequestBatch starts a batch of requests for this client.
   205  func (c *Client) NewRequestBatch() *RequestBatch {
   206  	return &RequestBatch{
   207  		requests: make([]*jsonRPCBufferedRequest, 0),
   208  		client:   c,
   209  	}
   210  }
   211  
   212  func (c *Client) sendBatch(requests []*jsonRPCBufferedRequest) ([]interface{}, error) {
   213  	reqs := make([]types.RPCRequest, 0, len(requests))
   214  	results := make([]interface{}, 0, len(requests))
   215  	for _, req := range requests {
   216  		reqs = append(reqs, req.request)
   217  		results = append(results, req.result)
   218  	}
   219  
   220  	// serialize the array of requests into a single JSON object
   221  	requestBytes, err := json.Marshal(reqs)
   222  	if err != nil {
   223  		return nil, fmt.Errorf("failed to marshal requests: %w", err)
   224  	}
   225  
   226  	httpRequest, err := http.NewRequest(http.MethodPost, c.address, bytes.NewBuffer(requestBytes))
   227  	if err != nil {
   228  		return nil, fmt.Errorf("request failed: %w", err)
   229  	}
   230  	httpRequest.Header.Set("Content-Type", "text/json")
   231  	if c.username != "" || c.password != "" {
   232  		httpRequest.SetBasicAuth(c.username, c.password)
   233  	}
   234  	httpResponse, err := c.client.Do(httpRequest)
   235  	if err != nil {
   236  		return nil, fmt.Errorf("post failed: %w", err)
   237  	}
   238  	defer httpResponse.Body.Close() // nolint: errcheck
   239  
   240  	responseBytes, err := ioutil.ReadAll(httpResponse.Body)
   241  	if err != nil {
   242  		return nil, fmt.Errorf("failed to read response body: %w", err)
   243  	}
   244  
   245  	// collect ids to check responses IDs in unmarshalResponseBytesArray
   246  	ids := make([]types.JSONRPCIntID, len(requests))
   247  	for i, req := range requests {
   248  		ids[i] = req.request.ID.(types.JSONRPCIntID)
   249  	}
   250  
   251  	return unmarshalResponseBytesArray(c.cdc, responseBytes, ids, results)
   252  }
   253  
   254  func (c *Client) nextRequestID() types.JSONRPCIntID {
   255  	c.mtx.Lock()
   256  	id := c.nextReqID
   257  	c.nextReqID++
   258  	c.mtx.Unlock()
   259  	return types.JSONRPCIntID(id)
   260  }
   261  
   262  //------------------------------------------------------------------------------------
   263  
   264  // jsonRPCBufferedRequest encapsulates a single buffered request, as well as its
   265  // anticipated response structure.
   266  type jsonRPCBufferedRequest struct {
   267  	request types.RPCRequest
   268  	result  interface{} // The result will be deserialized into this object.
   269  }
   270  
   271  // RequestBatch allows us to buffer multiple request/response structures
   272  // into a single batch request. Note that this batch acts like a FIFO queue, and
   273  // is thread-safe.
   274  type RequestBatch struct {
   275  	client *Client
   276  
   277  	mtx      sync.Mutex
   278  	requests []*jsonRPCBufferedRequest
   279  }
   280  
   281  // Count returns the number of enqueued requests waiting to be sent.
   282  func (b *RequestBatch) Count() int {
   283  	b.mtx.Lock()
   284  	defer b.mtx.Unlock()
   285  	return len(b.requests)
   286  }
   287  
   288  func (b *RequestBatch) enqueue(req *jsonRPCBufferedRequest) {
   289  	b.mtx.Lock()
   290  	defer b.mtx.Unlock()
   291  	b.requests = append(b.requests, req)
   292  }
   293  
   294  // Clear empties out the request batch.
   295  func (b *RequestBatch) Clear() int {
   296  	b.mtx.Lock()
   297  	defer b.mtx.Unlock()
   298  	return b.clear()
   299  }
   300  
   301  func (b *RequestBatch) clear() int {
   302  	count := len(b.requests)
   303  	b.requests = make([]*jsonRPCBufferedRequest, 0)
   304  	return count
   305  }
   306  
   307  // Send will attempt to send the current batch of enqueued requests, and then
   308  // will clear out the requests once done. On success, this returns the
   309  // deserialized list of results from each of the enqueued requests.
   310  func (b *RequestBatch) Send() ([]interface{}, error) {
   311  	b.mtx.Lock()
   312  	defer func() {
   313  		b.clear()
   314  		b.mtx.Unlock()
   315  	}()
   316  	return b.client.sendBatch(b.requests)
   317  }
   318  
   319  // Call enqueues a request to call the given RPC method with the specified
   320  // parameters, in the same way that the `Client.Call` function would.
   321  func (b *RequestBatch) Call(
   322  	method string,
   323  	params map[string]interface{},
   324  	result interface{},
   325  ) (interface{}, error) {
   326  	id := b.client.nextRequestID()
   327  	request, err := types.MapToRequest(b.client.cdc, id, method, params)
   328  	if err != nil {
   329  		return nil, err
   330  	}
   331  	b.enqueue(&jsonRPCBufferedRequest{request: request, result: result})
   332  	return result, nil
   333  }
   334  
   335  //-------------------------------------------------------------
   336  
   337  func makeHTTPDialer(remoteAddr string) (func(string, string) (net.Conn, error), error) {
   338  	u, err := newParsedURL(remoteAddr)
   339  	if err != nil {
   340  		return nil, err
   341  	}
   342  
   343  	protocol := u.Scheme
   344  
   345  	// accept http(s) as an alias for tcp
   346  	switch protocol {
   347  	case protoHTTP, protoHTTPS:
   348  		protocol = protoTCP
   349  	}
   350  
   351  	dialFn := func(proto, addr string) (net.Conn, error) {
   352  		return net.Dial(protocol, u.GetHostWithPath())
   353  	}
   354  
   355  	return dialFn, nil
   356  }
   357  
   358  // DefaultHTTPClient is used to create an http client with some default parameters.
   359  // We overwrite the http.Client.Dial so we can do http over tcp or unix.
   360  // remoteAddr should be fully featured (eg. with tcp:// or unix://).
   361  // An error will be returned in case of invalid remoteAddr.
   362  func DefaultHTTPClient(remoteAddr string) (*http.Client, error) {
   363  	dialFn, err := makeHTTPDialer(remoteAddr)
   364  	if err != nil {
   365  		return nil, err
   366  	}
   367  
   368  	client := &http.Client{
   369  		Transport: &http.Transport{
   370  			// Set to true to prevent GZIP-bomb DoS attacks
   371  			DisableCompression: true,
   372  			Dial:               dialFn,
   373  		},
   374  	}
   375  
   376  	return client, nil
   377  }