github.com/ari-anchor/sei-tendermint@v0.0.0-20230519144642-dc826b7b56bb/rpc/jsonrpc/client/http_json_client.go (about)

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