github.com/badrootd/celestia-core@v0.0.0-20240305091328-aa4207a4b25d/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/celestia-core/libs/sync" 15 types "github.com/badrootd/celestia-core/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 // New returns a Client pointed at the given address. 143 // An error is returned on invalid remote. The function panics when remote is nil. 144 func New(remote string) (*Client, error) { 145 httpClient, err := DefaultHTTPClient(remote) 146 if err != nil { 147 return nil, err 148 } 149 return NewWithHTTPClient(remote, httpClient) 150 } 151 152 // NewWithHTTPClient returns a Client pointed at the given 153 // address using a custom http client. An error is returned on invalid remote. 154 // The function panics when remote is nil. 155 func NewWithHTTPClient(remote string, client *http.Client) (*Client, error) { 156 if client == nil { 157 panic("nil http.Client provided") 158 } 159 160 parsedURL, err := newParsedURL(remote) 161 if err != nil { 162 return nil, fmt.Errorf("invalid remote %s: %s", remote, err) 163 } 164 165 parsedURL.SetDefaultSchemeHTTP() 166 167 address := parsedURL.GetTrimmedURL() 168 username := parsedURL.User.Username() 169 password, _ := parsedURL.User.Password() 170 171 rpcClient := &Client{ 172 address: address, 173 username: username, 174 password: password, 175 client: client, 176 } 177 178 return rpcClient, nil 179 } 180 181 // Call issues a POST HTTP request. Requests are JSON encoded. Content-Type: 182 // application/json. 183 func (c *Client) Call( 184 ctx context.Context, 185 method string, 186 params map[string]interface{}, 187 result interface{}, 188 ) (interface{}, error) { 189 id := c.nextRequestID() 190 191 request, err := types.MapToRequest(id, method, params) 192 if err != nil { 193 return nil, fmt.Errorf("failed to encode params: %w", err) 194 } 195 196 requestBytes, err := json.Marshal(request) 197 if err != nil { 198 return nil, fmt.Errorf("failed to marshal request: %w", err) 199 } 200 201 requestBuf := bytes.NewBuffer(requestBytes) 202 httpRequest, err := http.NewRequestWithContext(ctx, http.MethodPost, c.address, requestBuf) 203 if err != nil { 204 return nil, fmt.Errorf("request failed: %w", err) 205 } 206 207 httpRequest.Header.Set("Content-Type", "application/json") 208 209 if c.username != "" || c.password != "" { 210 httpRequest.SetBasicAuth(c.username, c.password) 211 } 212 213 httpResponse, err := c.client.Do(httpRequest) 214 if err != nil { 215 return nil, fmt.Errorf("post failed: %w", err) 216 } 217 defer httpResponse.Body.Close() 218 219 responseBytes, err := io.ReadAll(httpResponse.Body) 220 if err != nil { 221 return nil, fmt.Errorf("%s. Failed to read response body: %w", getHTTPRespErrPrefix(httpResponse), err) 222 } 223 224 res, err := unmarshalResponseBytes(responseBytes, id, result) 225 if err != nil { 226 return nil, fmt.Errorf("%s. %w", getHTTPRespErrPrefix(httpResponse), err) 227 } 228 return res, nil 229 } 230 231 func getHTTPRespErrPrefix(resp *http.Response) string { 232 return fmt.Sprintf("error in json rpc client, with http response metadata: (Status: %s, Protocol %s)", resp.Status, resp.Proto) 233 } 234 235 // NewRequestBatch starts a batch of requests for this client. 236 func (c *Client) NewRequestBatch() *RequestBatch { 237 return &RequestBatch{ 238 requests: make([]*jsonRPCBufferedRequest, 0), 239 client: c, 240 } 241 } 242 243 func (c *Client) sendBatch(ctx context.Context, requests []*jsonRPCBufferedRequest) ([]interface{}, error) { 244 reqs := make([]types.RPCRequest, 0, len(requests)) 245 results := make([]interface{}, 0, len(requests)) 246 for _, req := range requests { 247 reqs = append(reqs, req.request) 248 results = append(results, req.result) 249 } 250 251 // serialize the array of requests into a single JSON object 252 requestBytes, err := json.Marshal(reqs) 253 if err != nil { 254 return nil, fmt.Errorf("json marshal: %w", err) 255 } 256 257 httpRequest, err := http.NewRequestWithContext(ctx, http.MethodPost, c.address, bytes.NewBuffer(requestBytes)) 258 if err != nil { 259 return nil, fmt.Errorf("new request: %w", err) 260 } 261 262 httpRequest.Header.Set("Content-Type", "application/json") 263 264 if c.username != "" || c.password != "" { 265 httpRequest.SetBasicAuth(c.username, c.password) 266 } 267 268 httpResponse, err := c.client.Do(httpRequest) 269 if err != nil { 270 return nil, fmt.Errorf("post: %w", err) 271 } 272 273 defer httpResponse.Body.Close() 274 275 responseBytes, err := io.ReadAll(httpResponse.Body) 276 if err != nil { 277 return nil, fmt.Errorf("read response body: %w", err) 278 } 279 280 // collect ids to check responses IDs in unmarshalResponseBytesArray 281 ids := make([]types.JSONRPCIntID, len(requests)) 282 for i, req := range requests { 283 ids[i] = req.request.ID.(types.JSONRPCIntID) 284 } 285 286 return unmarshalResponseBytesArray(responseBytes, ids, results) 287 } 288 289 func (c *Client) nextRequestID() types.JSONRPCIntID { 290 c.mtx.Lock() 291 id := c.nextReqID 292 c.nextReqID++ 293 c.mtx.Unlock() 294 return types.JSONRPCIntID(id) 295 } 296 297 //------------------------------------------------------------------------------------ 298 299 // jsonRPCBufferedRequest encapsulates a single buffered request, as well as its 300 // anticipated response structure. 301 type jsonRPCBufferedRequest struct { 302 request types.RPCRequest 303 result interface{} // The result will be deserialized into this object. 304 } 305 306 // RequestBatch allows us to buffer multiple request/response structures 307 // into a single batch request. Note that this batch acts like a FIFO queue, and 308 // is thread-safe. 309 type RequestBatch struct { 310 client *Client 311 312 mtx cmtsync.Mutex 313 requests []*jsonRPCBufferedRequest 314 } 315 316 // Count returns the number of enqueued requests waiting to be sent. 317 func (b *RequestBatch) Count() int { 318 b.mtx.Lock() 319 defer b.mtx.Unlock() 320 return len(b.requests) 321 } 322 323 func (b *RequestBatch) enqueue(req *jsonRPCBufferedRequest) { 324 b.mtx.Lock() 325 defer b.mtx.Unlock() 326 b.requests = append(b.requests, req) 327 } 328 329 // Clear empties out the request batch. 330 func (b *RequestBatch) Clear() int { 331 b.mtx.Lock() 332 defer b.mtx.Unlock() 333 return b.clear() 334 } 335 336 func (b *RequestBatch) clear() int { 337 count := len(b.requests) 338 b.requests = make([]*jsonRPCBufferedRequest, 0) 339 return count 340 } 341 342 // Send will attempt to send the current batch of enqueued requests, and then 343 // will clear out the requests once done. On success, this returns the 344 // deserialized list of results from each of the enqueued requests. 345 func (b *RequestBatch) Send(ctx context.Context) ([]interface{}, error) { 346 b.mtx.Lock() 347 defer func() { 348 b.clear() 349 b.mtx.Unlock() 350 }() 351 return b.client.sendBatch(ctx, b.requests) 352 } 353 354 // Call enqueues a request to call the given RPC method with the specified 355 // parameters, in the same way that the `Client.Call` function would. 356 func (b *RequestBatch) Call( 357 _ context.Context, 358 method string, 359 params map[string]interface{}, 360 result interface{}, 361 ) (interface{}, error) { 362 id := b.client.nextRequestID() 363 request, err := types.MapToRequest(id, method, params) 364 if err != nil { 365 return nil, err 366 } 367 b.enqueue(&jsonRPCBufferedRequest{request: request, result: result}) 368 return result, nil 369 } 370 371 //------------------------------------------------------------- 372 373 func makeHTTPDialer(remoteAddr string) (func(string, string) (net.Conn, error), error) { 374 u, err := newParsedURL(remoteAddr) 375 if err != nil { 376 return nil, err 377 } 378 379 protocol := u.Scheme 380 381 // accept http(s) as an alias for tcp 382 switch protocol { 383 case protoHTTP, protoHTTPS: 384 protocol = protoTCP 385 } 386 387 dialFn := func(proto, addr string) (net.Conn, error) { 388 return net.Dial(protocol, u.GetDialAddress()) 389 } 390 391 return dialFn, nil 392 } 393 394 // DefaultHTTPClient is used to create an http client with some default parameters. 395 // We overwrite the http.Client.Dial so we can do http over tcp or unix. 396 // remoteAddr should be fully featured (eg. with tcp:// or unix://). 397 // An error will be returned in case of invalid remoteAddr. 398 func DefaultHTTPClient(remoteAddr string) (*http.Client, error) { 399 dialFn, err := makeHTTPDialer(remoteAddr) 400 if err != nil { 401 return nil, err 402 } 403 404 client := &http.Client{ 405 Transport: &http.Transport{ 406 // Set to true to prevent GZIP-bomb DoS attacks 407 DisableCompression: true, 408 Dial: dialFn, 409 }, 410 } 411 412 return client, nil 413 }