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