github.com/ari-anchor/sei-tendermint@v0.0.0-20230519144642-dc826b7b56bb/rpc/jsonrpc/client/http_json_client.go (about) 1 package client 2 3 import ( 4 "bytes" 5 "context" 6 "encoding/json" 7 "errors" 8 "fmt" 9 "io" 10 "net" 11 "net/http" 12 "net/url" 13 "strings" 14 "sync" 15 "time" 16 17 rpctypes "github.com/ari-anchor/sei-tendermint/rpc/jsonrpc/types" 18 ) 19 20 const ( 21 protoHTTP = "http" 22 protoHTTPS = "https" 23 protoWSS = "wss" 24 protoWS = "ws" 25 protoTCP = "tcp" 26 protoUNIX = "unix" 27 ) 28 29 //------------------------------------------------------------- 30 31 // Parsed URL structure 32 type parsedURL struct { 33 url.URL 34 35 isUnixSocket bool 36 } 37 38 // Parse URL and set defaults 39 func newParsedURL(remoteAddr string) (*parsedURL, error) { 40 u, err := url.Parse(remoteAddr) 41 if err != nil { 42 return nil, err 43 } 44 45 // default to tcp if nothing specified 46 if u.Scheme == "" { 47 u.Scheme = protoTCP 48 } 49 50 pu := &parsedURL{ 51 URL: *u, 52 isUnixSocket: false, 53 } 54 55 if u.Scheme == protoUNIX { 56 pu.isUnixSocket = true 57 } 58 59 return pu, nil 60 } 61 62 // Change protocol to HTTP for unknown protocols and TCP protocol - useful for RPC connections 63 func (u *parsedURL) SetDefaultSchemeHTTP() { 64 // protocol to use for http operations, to support both http and https 65 switch u.Scheme { 66 case protoHTTP, protoHTTPS, protoWS, protoWSS: 67 // known protocols not changed 68 default: 69 // default to http for unknown protocols (ex. tcp) 70 u.Scheme = protoHTTP 71 } 72 } 73 74 // Get full address without the protocol - useful for Dialer connections 75 func (u parsedURL) GetHostWithPath() string { 76 // Remove protocol, userinfo and # fragment, assume opaque is empty 77 return u.Host + u.EscapedPath() 78 } 79 80 // Get a trimmed address - useful for WS connections 81 func (u parsedURL) GetTrimmedHostWithPath() string { 82 // if it's not an unix socket we return the normal URL 83 if !u.isUnixSocket { 84 return u.GetHostWithPath() 85 } 86 // if it's a unix socket we replace the host slashes with a period 87 // this is because otherwise the http.Client would think that the 88 // domain is invalid. 89 return strings.ReplaceAll(u.GetHostWithPath(), "/", ".") 90 } 91 92 // GetDialAddress returns the endpoint to dial for the parsed URL 93 func (u parsedURL) GetDialAddress() string { 94 // if it's not a unix socket we return the host, example: localhost:443 95 if !u.isUnixSocket { 96 return u.Host 97 } 98 // otherwise we return the path of the unix socket, ex /tmp/socket 99 return u.GetHostWithPath() 100 } 101 102 // Get a trimmed address with protocol - useful as address in RPC connections 103 func (u parsedURL) GetTrimmedURL() string { 104 return u.Scheme + "://" + u.GetTrimmedHostWithPath() 105 } 106 107 //------------------------------------------------------------- 108 109 // A Caller handles the round trip of a single JSON-RPC request. The 110 // implementation is responsible for assigning request IDs, marshaling 111 // parameters, and unmarshaling results. 112 type Caller interface { 113 // Call sends a new request for method to the server with the given 114 // parameters. If params == nil, the request has empty parameters. 115 // If result == nil, any result value must be discarded without error. 116 // Otherwise the concrete value of result must be a pointer. 117 Call(ctx context.Context, method string, params, result interface{}) error 118 } 119 120 //------------------------------------------------------------- 121 122 // Client is a JSON-RPC client, which sends POST HTTP requests to the 123 // remote server. 124 // 125 // Client is safe for concurrent use by multiple goroutines. 126 type Client struct { 127 address string 128 username string 129 password string 130 131 client *http.Client 132 133 mtx sync.Mutex 134 nextReqID int 135 } 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 address using a 153 // custom HTTP client. It reports an error if c == nil or if remote is not a 154 // valid URL. 155 func NewWithHTTPClient(remote string, c *http.Client) (*Client, error) { 156 if c == nil { 157 return nil, errors.New("nil client") 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: c, 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(ctx context.Context, method string, params, result interface{}) error { 184 id := c.nextRequestID() 185 186 request := rpctypes.NewRequest(id) 187 if err := request.SetMethodAndParams(method, params); err != nil { 188 return fmt.Errorf("failed to encode params: %w", err) 189 } 190 191 requestBytes, err := json.Marshal(request) 192 if err != nil { 193 return fmt.Errorf("failed to marshal request: %w", err) 194 } 195 196 requestBuf := bytes.NewBuffer(requestBytes) 197 httpRequest, err := http.NewRequestWithContext(ctx, http.MethodPost, c.address, requestBuf) 198 if err != nil { 199 return fmt.Errorf("request setup failed: %w", err) 200 } 201 202 httpRequest.Header.Set("Content-Type", "application/json") 203 204 if c.username != "" || c.password != "" { 205 httpRequest.SetBasicAuth(c.username, c.password) 206 } 207 208 httpResponse, err := c.client.Do(httpRequest) 209 if err != nil { 210 return err 211 } 212 213 responseBytes, err := io.ReadAll(httpResponse.Body) 214 httpResponse.Body.Close() 215 if err != nil { 216 return fmt.Errorf("reading response body: %w", err) 217 } 218 219 return unmarshalResponseBytes(responseBytes, request.ID(), result) 220 } 221 222 // NewRequestBatch starts a batch of requests for this client. 223 func (c *Client) NewRequestBatch() *RequestBatch { 224 return &RequestBatch{ 225 requests: make([]*jsonRPCBufferedRequest, 0), 226 client: c, 227 } 228 } 229 230 func (c *Client) sendBatch(ctx context.Context, requests []*jsonRPCBufferedRequest) ([]interface{}, error) { 231 reqs := make([]rpctypes.RPCRequest, 0, len(requests)) 232 results := make([]interface{}, 0, len(requests)) 233 for _, req := range requests { 234 reqs = append(reqs, req.request) 235 results = append(results, req.result) 236 } 237 238 // serialize the array of requests into a single JSON object 239 requestBytes, err := json.Marshal(reqs) 240 if err != nil { 241 return nil, fmt.Errorf("json marshal: %w", err) 242 } 243 244 httpRequest, err := http.NewRequestWithContext(ctx, http.MethodPost, c.address, bytes.NewBuffer(requestBytes)) 245 if err != nil { 246 return nil, fmt.Errorf("new request: %w", err) 247 } 248 249 httpRequest.Header.Set("Content-Type", "application/json") 250 251 if c.username != "" || c.password != "" { 252 httpRequest.SetBasicAuth(c.username, c.password) 253 } 254 255 httpResponse, err := c.client.Do(httpRequest) 256 if err != nil { 257 return nil, fmt.Errorf("post: %w", err) 258 } 259 260 responseBytes, err := io.ReadAll(httpResponse.Body) 261 httpResponse.Body.Close() 262 if err != nil { 263 return nil, fmt.Errorf("reading response body: %w", err) 264 } 265 266 // collect ids to check responses IDs in unmarshalResponseBytesArray 267 ids := make([]string, len(requests)) 268 for i, req := range requests { 269 ids[i] = req.request.ID() 270 } 271 272 if err := unmarshalResponseBytesArray(responseBytes, ids, results); err != nil { 273 return nil, err 274 } 275 return results, nil 276 } 277 278 func (c *Client) nextRequestID() int { 279 c.mtx.Lock() 280 defer c.mtx.Unlock() 281 id := c.nextReqID 282 c.nextReqID++ 283 return id 284 } 285 286 //------------------------------------------------------------------------------------ 287 288 // jsonRPCBufferedRequest encapsulates a single buffered request, as well as its 289 // anticipated response structure. 290 type jsonRPCBufferedRequest struct { 291 request rpctypes.RPCRequest 292 result interface{} // The result will be deserialized into this object. 293 } 294 295 // RequestBatch allows us to buffer multiple request/response structures 296 // into a single batch request. Note that this batch acts like a FIFO queue, and 297 // is thread-safe. 298 type RequestBatch struct { 299 client *Client 300 301 mtx sync.Mutex 302 requests []*jsonRPCBufferedRequest 303 } 304 305 // Count returns the number of enqueued requests waiting to be sent. 306 func (b *RequestBatch) Count() int { 307 b.mtx.Lock() 308 defer b.mtx.Unlock() 309 return len(b.requests) 310 } 311 312 func (b *RequestBatch) enqueue(req *jsonRPCBufferedRequest) { 313 b.mtx.Lock() 314 defer b.mtx.Unlock() 315 b.requests = append(b.requests, req) 316 } 317 318 // Clear empties out the request batch. 319 func (b *RequestBatch) Clear() int { 320 b.mtx.Lock() 321 defer b.mtx.Unlock() 322 return b.clear() 323 } 324 325 func (b *RequestBatch) clear() int { 326 count := len(b.requests) 327 b.requests = make([]*jsonRPCBufferedRequest, 0) 328 return count 329 } 330 331 // Send will attempt to send the current batch of enqueued requests, and then 332 // will clear out the requests once done. On success, this returns the 333 // deserialized list of results from each of the enqueued requests. 334 func (b *RequestBatch) Send(ctx context.Context) ([]interface{}, error) { 335 b.mtx.Lock() 336 defer func() { 337 b.clear() 338 b.mtx.Unlock() 339 }() 340 return b.client.sendBatch(ctx, b.requests) 341 } 342 343 // Call enqueues a request to call the given RPC method with the specified 344 // parameters, in the same way that the `Client.Call` function would. 345 func (b *RequestBatch) Call(_ context.Context, method string, params, result interface{}) error { 346 request := rpctypes.NewRequest(b.client.nextRequestID()) 347 if err := request.SetMethodAndParams(method, params); err != nil { 348 return err 349 } 350 b.enqueue(&jsonRPCBufferedRequest{request: request, result: result}) 351 return nil 352 } 353 354 //------------------------------------------------------------- 355 356 func makeHTTPDialer(remoteAddr string) (func(string, string) (net.Conn, error), error) { 357 u, err := newParsedURL(remoteAddr) 358 if err != nil { 359 return nil, err 360 } 361 362 protocol := u.Scheme 363 padding := u.Scheme 364 365 // accept http(s) as an alias for tcp 366 switch protocol { 367 case protoHTTP, protoHTTPS: 368 protocol = protoTCP 369 } 370 371 dialFn := func(proto, addr string) (net.Conn, error) { 372 var timeout = 10 * time.Second 373 if !u.isUnixSocket && strings.LastIndex(u.Host, ":") == -1 { 374 u.Host = fmt.Sprintf("%s:%s", u.Host, padding) 375 return net.DialTimeout(protocol, u.GetDialAddress(), timeout) 376 } 377 378 return net.DialTimeout(protocol, u.GetDialAddress(), timeout) 379 } 380 381 return dialFn, nil 382 } 383 384 // DefaultHTTPClient is used to create an http client with some default parameters. 385 // We overwrite the http.Client.Dial so we can do http over tcp or unix. 386 // remoteAddr should be fully featured (eg. with tcp:// or unix://). 387 // An error will be returned in case of invalid remoteAddr. 388 func DefaultHTTPClient(remoteAddr string) (*http.Client, error) { 389 dialFn, err := makeHTTPDialer(remoteAddr) 390 if err != nil { 391 return nil, err 392 } 393 394 client := &http.Client{ 395 Transport: &http.Transport{ 396 // Set to true to prevent GZIP-bomb DoS attacks 397 DisableCompression: true, 398 Dial: dialFn, 399 }, 400 } 401 402 return client, nil 403 }