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