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