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