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 }