github.com/badrootd/nibiru-cometbft@v0.37.5-0.20240307173500-2a75559eee9b/rpc/jsonrpc/client/http_json_client.go (about)

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