github.com/gnolang/gno@v0.0.0-20240520182011-228e9d0192ce/tm2/pkg/bft/rpc/lib/client/http/client.go (about)

     1  package http
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"encoding/json"
     7  	"errors"
     8  	"fmt"
     9  	"io"
    10  	"net"
    11  	"net/http"
    12  	"strings"
    13  
    14  	types "github.com/gnolang/gno/tm2/pkg/bft/rpc/lib/types"
    15  )
    16  
    17  const (
    18  	protoHTTP  = "http"
    19  	protoHTTPS = "https"
    20  	protoWSS   = "wss"
    21  	protoWS    = "ws"
    22  	protoTCP   = "tcp"
    23  )
    24  
    25  var (
    26  	ErrRequestResponseIDMismatch = errors.New("http request / response ID mismatch")
    27  	ErrInvalidBatchResponse      = errors.New("invalid http batch response size")
    28  )
    29  
    30  // Client is an HTTP client implementation
    31  type Client struct {
    32  	rpcURL string // the remote RPC URL of the node
    33  
    34  	client *http.Client
    35  }
    36  
    37  // NewClient initializes and creates a new HTTP RPC client
    38  func NewClient(rpcURL string) (*Client, error) {
    39  	// Parse the RPC URL
    40  	address, err := toClientAddress(rpcURL)
    41  	if err != nil {
    42  		return nil, fmt.Errorf("invalid RPC URL, %w", err)
    43  	}
    44  
    45  	c := &Client{
    46  		rpcURL: address,
    47  		client: defaultHTTPClient(rpcURL),
    48  	}
    49  
    50  	return c, nil
    51  }
    52  
    53  // SendRequest sends a single RPC request to the server
    54  func (c *Client) SendRequest(ctx context.Context, request types.RPCRequest) (*types.RPCResponse, error) {
    55  	// Send the request
    56  	response, err := sendRequestCommon[types.RPCRequest, *types.RPCResponse](ctx, c.client, c.rpcURL, request)
    57  	if err != nil {
    58  		return nil, err
    59  	}
    60  
    61  	// Make sure the ID matches
    62  	if response.ID != response.ID {
    63  		return nil, ErrRequestResponseIDMismatch
    64  	}
    65  
    66  	return response, nil
    67  }
    68  
    69  // SendBatch sends a single RPC batch request to the server
    70  func (c *Client) SendBatch(ctx context.Context, requests types.RPCRequests) (types.RPCResponses, error) {
    71  	// Send the batch
    72  	responses, err := sendRequestCommon[types.RPCRequests, types.RPCResponses](ctx, c.client, c.rpcURL, requests)
    73  	if err != nil {
    74  		return nil, err
    75  	}
    76  
    77  	// Make sure the length matches
    78  	if len(responses) != len(requests) {
    79  		return nil, ErrInvalidBatchResponse
    80  	}
    81  
    82  	// Make sure the IDs match
    83  	for index, response := range responses {
    84  		if requests[index].ID != response.ID {
    85  			return nil, ErrRequestResponseIDMismatch
    86  		}
    87  	}
    88  
    89  	return responses, nil
    90  }
    91  
    92  // Close has no effect on an HTTP client
    93  func (c *Client) Close() error {
    94  	return nil
    95  }
    96  
    97  type (
    98  	requestType interface {
    99  		types.RPCRequest | types.RPCRequests
   100  	}
   101  
   102  	responseType interface {
   103  		*types.RPCResponse | types.RPCResponses
   104  	}
   105  )
   106  
   107  // sendRequestCommon executes the common request sending
   108  func sendRequestCommon[T requestType, R responseType](
   109  	ctx context.Context,
   110  	client *http.Client,
   111  	rpcURL string,
   112  	request T,
   113  ) (R, error) {
   114  	// Marshal the request
   115  	requestBytes, err := json.Marshal(request)
   116  	if err != nil {
   117  		return nil, fmt.Errorf("unable to JSON-marshal the request, %w", err)
   118  	}
   119  
   120  	// Craft the request
   121  	req, err := http.NewRequest(
   122  		http.MethodPost,
   123  		rpcURL,
   124  		bytes.NewBuffer(requestBytes),
   125  	)
   126  	if err != nil {
   127  		return nil, fmt.Errorf("unable to create request, %w", err)
   128  	}
   129  
   130  	// Set the header content type
   131  	req.Header.Set("Content-Type", "application/json")
   132  
   133  	// Execute the request
   134  	httpResponse, err := client.Do(req.WithContext(ctx))
   135  	if err != nil {
   136  		return nil, fmt.Errorf("unable to send request, %w", err)
   137  	}
   138  	defer httpResponse.Body.Close() //nolint: errcheck
   139  
   140  	// Parse the response code
   141  	if !isOKStatus(httpResponse.StatusCode) {
   142  		return nil, fmt.Errorf("invalid status code received, %d", httpResponse.StatusCode)
   143  	}
   144  
   145  	// Parse the response body
   146  	responseBytes, err := io.ReadAll(httpResponse.Body)
   147  	if err != nil {
   148  		return nil, fmt.Errorf("unable to read response body, %w", err)
   149  	}
   150  
   151  	var response R
   152  
   153  	if err := json.Unmarshal(responseBytes, &response); err != nil {
   154  		return nil, fmt.Errorf("unable to unmarshal response body, %w", err)
   155  	}
   156  
   157  	return response, nil
   158  }
   159  
   160  // DefaultHTTPClient is used to create an http client with some default parameters.
   161  // We overwrite the http.Client.Dial so we can do http over tcp or unix.
   162  // remoteAddr should be fully featured (eg. with tcp:// or unix://)
   163  func defaultHTTPClient(remoteAddr string) *http.Client {
   164  	return &http.Client{
   165  		Transport: &http.Transport{
   166  			// Set to true to prevent GZIP-bomb DoS attacks
   167  			DisableCompression: true,
   168  			DialContext: func(_ context.Context, network, addr string) (net.Conn, error) {
   169  				return makeHTTPDialer(remoteAddr)(network, addr)
   170  			},
   171  		},
   172  	}
   173  }
   174  
   175  func makeHTTPDialer(remoteAddr string) func(string, string) (net.Conn, error) {
   176  	protocol, address, err := parseRemoteAddr(remoteAddr)
   177  	if err != nil {
   178  		return func(_ string, _ string) (net.Conn, error) {
   179  			return nil, err
   180  		}
   181  	}
   182  
   183  	// net.Dial doesn't understand http/https, so change it to TCP
   184  	switch protocol {
   185  	case protoHTTP, protoHTTPS:
   186  		protocol = protoTCP
   187  	}
   188  
   189  	return func(proto, addr string) (net.Conn, error) {
   190  		return net.Dial(protocol, address)
   191  	}
   192  }
   193  
   194  // protocol - client's protocol (for example, "http", "https", "wss", "ws", "tcp")
   195  // trimmedS - rest of the address (for example, "192.0.2.1:25", "[2001:db8::1]:80") with "/" replaced with "."
   196  func toClientAddrAndParse(remoteAddr string) (string, string, error) {
   197  	protocol, address, err := parseRemoteAddr(remoteAddr)
   198  	if err != nil {
   199  		return "", "", err
   200  	}
   201  
   202  	// protocol to use for http operations, to support both http and https
   203  	var clientProtocol string
   204  	// default to http for unknown protocols (ex. tcp)
   205  	switch protocol {
   206  	case protoHTTP, protoHTTPS, protoWS, protoWSS:
   207  		clientProtocol = protocol
   208  	default:
   209  		clientProtocol = protoHTTP
   210  	}
   211  
   212  	// replace / with . for http requests (kvstore domain)
   213  	trimmedAddress := strings.Replace(address, "/", ".", -1)
   214  
   215  	return clientProtocol, trimmedAddress, nil
   216  }
   217  
   218  func toClientAddress(remoteAddr string) (string, error) {
   219  	clientProtocol, trimmedAddress, err := toClientAddrAndParse(remoteAddr)
   220  	if err != nil {
   221  		return "", err
   222  	}
   223  
   224  	return clientProtocol + "://" + trimmedAddress, nil
   225  }
   226  
   227  // network - name of the network (for example, "tcp", "unix")
   228  // s - rest of the address (for example, "192.0.2.1:25", "[2001:db8::1]:80")
   229  // TODO: Deprecate support for IP:PORT or /path/to/socket
   230  func parseRemoteAddr(remoteAddr string) (network string, s string, err error) {
   231  	parts := strings.SplitN(remoteAddr, "://", 2)
   232  	var protocol, address string
   233  	switch len(parts) {
   234  	case 1:
   235  		// default to tcp if nothing specified
   236  		protocol, address = protoTCP, remoteAddr
   237  	case 2:
   238  		protocol, address = parts[0], parts[1]
   239  	}
   240  	return protocol, address, nil
   241  }
   242  
   243  // isOKStatus returns a boolean indicating if the response
   244  // status code is between 200 and 299 (inclusive)
   245  func isOKStatus(code int) bool { return code >= 200 && code <= 299 }