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