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