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