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 }