github.com/franono/tendermint@v0.32.2-0.20200527150959-749313264ce9/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 "sync" 13 14 amino "github.com/tendermint/go-amino" 15 16 types "github.com/franono/tendermint/rpc/jsonrpc/types" 17 ) 18 19 const ( 20 protoHTTP = "http" 21 protoHTTPS = "https" 22 protoWSS = "wss" 23 protoWS = "ws" 24 protoTCP = "tcp" 25 ) 26 27 //------------------------------------------------------------- 28 29 // Parsed URL structure 30 type parsedURL struct { 31 url.URL 32 } 33 34 // Parse URL and set defaults 35 func newParsedURL(remoteAddr string) (*parsedURL, error) { 36 u, err := url.Parse(remoteAddr) 37 if err != nil { 38 return nil, err 39 } 40 41 // default to tcp if nothing specified 42 if u.Scheme == "" { 43 u.Scheme = protoTCP 44 } 45 46 return &parsedURL{*u}, nil 47 } 48 49 // Change protocol to HTTP for unknown protocols and TCP protocol - useful for RPC connections 50 func (u *parsedURL) SetDefaultSchemeHTTP() { 51 // protocol to use for http operations, to support both http and https 52 switch u.Scheme { 53 case protoHTTP, protoHTTPS, protoWS, protoWSS: 54 // known protocols not changed 55 default: 56 // default to http for unknown protocols (ex. tcp) 57 u.Scheme = protoHTTP 58 } 59 } 60 61 // Get full address without the protocol - useful for Dialer connections 62 func (u parsedURL) GetHostWithPath() string { 63 // Remove protocol, userinfo and # fragment, assume opaque is empty 64 return u.Host + u.EscapedPath() 65 } 66 67 // Get a trimmed address - useful for WS connections 68 func (u parsedURL) GetTrimmedHostWithPath() string { 69 // replace / with . for http requests (kvstore domain) 70 return strings.Replace(u.GetHostWithPath(), "/", ".", -1) 71 } 72 73 // Get a trimmed address with protocol - useful as address in RPC connections 74 func (u parsedURL) GetTrimmedURL() string { 75 return u.Scheme + "://" + u.GetTrimmedHostWithPath() 76 } 77 78 //------------------------------------------------------------- 79 80 // HTTPClient is a common interface for JSON-RPC HTTP clients. 81 type HTTPClient interface { 82 // Call calls the given method with the params and returns a result. 83 Call(method string, params map[string]interface{}, result interface{}) (interface{}, error) 84 // Codec returns an amino codec used. 85 Codec() *amino.Codec 86 // SetCodec sets an amino codec. 87 SetCodec(*amino.Codec) 88 } 89 90 // Caller implementers can facilitate calling the JSON-RPC endpoint. 91 type Caller interface { 92 Call(method string, params map[string]interface{}, result interface{}) (interface{}, error) 93 } 94 95 //------------------------------------------------------------- 96 97 // Client is a JSON-RPC client, which sends POST HTTP requests to the 98 // remote server. 99 // 100 // Request values are amino encoded. Response is expected to be amino encoded. 101 // New amino codec is used if no other codec was set using SetCodec. 102 // 103 // Client is safe for concurrent use by multiple goroutines. 104 type Client struct { 105 address string 106 username string 107 password string 108 109 client *http.Client 110 cdc *amino.Codec 111 112 mtx sync.Mutex 113 nextReqID int 114 } 115 116 var _ HTTPClient = (*Client)(nil) 117 118 // Both Client and RequestBatch can facilitate calls to the JSON 119 // RPC endpoint. 120 var _ Caller = (*Client)(nil) 121 var _ Caller = (*RequestBatch)(nil) 122 123 // New returns a Client pointed at the given address. 124 // An error is returned on invalid remote. The function panics when remote is nil. 125 func New(remote string) (*Client, error) { 126 httpClient, err := DefaultHTTPClient(remote) 127 if err != nil { 128 return nil, err 129 } 130 return NewWithHTTPClient(remote, httpClient) 131 } 132 133 // NewWithHTTPClient returns a Client pointed at the given 134 // address using a custom http client. An error is returned on invalid remote. 135 // The function panics when remote is nil. 136 func NewWithHTTPClient(remote string, client *http.Client) (*Client, error) { 137 if client == nil { 138 panic("nil http.Client provided") 139 } 140 141 parsedURL, err := newParsedURL(remote) 142 if err != nil { 143 return nil, fmt.Errorf("invalid remote %s: %s", remote, err) 144 } 145 146 parsedURL.SetDefaultSchemeHTTP() 147 148 address := parsedURL.GetTrimmedURL() 149 username := parsedURL.User.Username() 150 password, _ := parsedURL.User.Password() 151 152 rpcClient := &Client{ 153 address: address, 154 username: username, 155 password: password, 156 client: client, 157 cdc: amino.NewCodec(), 158 } 159 160 return rpcClient, nil 161 } 162 163 // Call issues a POST HTTP request. Requests are JSON encoded. Content-Type: 164 // text/json. 165 func (c *Client) Call(method string, params map[string]interface{}, result interface{}) (interface{}, error) { 166 id := c.nextRequestID() 167 168 request, err := types.MapToRequest(c.cdc, id, method, params) 169 if err != nil { 170 return nil, fmt.Errorf("failed to encode params: %w", err) 171 } 172 173 requestBytes, err := json.Marshal(request) 174 if err != nil { 175 return nil, fmt.Errorf("failed to marshal request: %w", err) 176 } 177 178 requestBuf := bytes.NewBuffer(requestBytes) 179 httpRequest, err := http.NewRequest(http.MethodPost, c.address, requestBuf) 180 if err != nil { 181 return nil, fmt.Errorf("request failed: %w", err) 182 } 183 httpRequest.Header.Set("Content-Type", "text/json") 184 if c.username != "" || c.password != "" { 185 httpRequest.SetBasicAuth(c.username, c.password) 186 } 187 httpResponse, err := c.client.Do(httpRequest) 188 if err != nil { 189 return nil, fmt.Errorf("post failed: %w", err) 190 } 191 defer httpResponse.Body.Close() // nolint: errcheck 192 193 responseBytes, err := ioutil.ReadAll(httpResponse.Body) 194 if err != nil { 195 return nil, fmt.Errorf("failed to read response body: %w", err) 196 } 197 198 return unmarshalResponseBytes(c.cdc, responseBytes, id, result) 199 } 200 201 func (c *Client) Codec() *amino.Codec { return c.cdc } 202 func (c *Client) SetCodec(cdc *amino.Codec) { c.cdc = cdc } 203 204 // NewRequestBatch starts a batch of requests for this client. 205 func (c *Client) NewRequestBatch() *RequestBatch { 206 return &RequestBatch{ 207 requests: make([]*jsonRPCBufferedRequest, 0), 208 client: c, 209 } 210 } 211 212 func (c *Client) sendBatch(requests []*jsonRPCBufferedRequest) ([]interface{}, error) { 213 reqs := make([]types.RPCRequest, 0, len(requests)) 214 results := make([]interface{}, 0, len(requests)) 215 for _, req := range requests { 216 reqs = append(reqs, req.request) 217 results = append(results, req.result) 218 } 219 220 // serialize the array of requests into a single JSON object 221 requestBytes, err := json.Marshal(reqs) 222 if err != nil { 223 return nil, fmt.Errorf("failed to marshal requests: %w", err) 224 } 225 226 httpRequest, err := http.NewRequest(http.MethodPost, c.address, bytes.NewBuffer(requestBytes)) 227 if err != nil { 228 return nil, fmt.Errorf("request failed: %w", err) 229 } 230 httpRequest.Header.Set("Content-Type", "text/json") 231 if c.username != "" || c.password != "" { 232 httpRequest.SetBasicAuth(c.username, c.password) 233 } 234 httpResponse, err := c.client.Do(httpRequest) 235 if err != nil { 236 return nil, fmt.Errorf("post failed: %w", err) 237 } 238 defer httpResponse.Body.Close() // nolint: errcheck 239 240 responseBytes, err := ioutil.ReadAll(httpResponse.Body) 241 if err != nil { 242 return nil, fmt.Errorf("failed to 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(c.cdc, 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 sync.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() ([]interface{}, error) { 311 b.mtx.Lock() 312 defer func() { 313 b.clear() 314 b.mtx.Unlock() 315 }() 316 return b.client.sendBatch(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 method string, 323 params map[string]interface{}, 324 result interface{}, 325 ) (interface{}, error) { 326 id := b.client.nextRequestID() 327 request, err := types.MapToRequest(b.client.cdc, id, method, params) 328 if err != nil { 329 return nil, err 330 } 331 b.enqueue(&jsonRPCBufferedRequest{request: request, result: result}) 332 return result, nil 333 } 334 335 //------------------------------------------------------------- 336 337 func makeHTTPDialer(remoteAddr string) (func(string, string) (net.Conn, error), error) { 338 u, err := newParsedURL(remoteAddr) 339 if err != nil { 340 return nil, err 341 } 342 343 protocol := u.Scheme 344 345 // accept http(s) as an alias for tcp 346 switch protocol { 347 case protoHTTP, protoHTTPS: 348 protocol = protoTCP 349 } 350 351 dialFn := func(proto, addr string) (net.Conn, error) { 352 return net.Dial(protocol, u.GetHostWithPath()) 353 } 354 355 return dialFn, nil 356 } 357 358 // DefaultHTTPClient is used to create an http client with some default parameters. 359 // We overwrite the http.Client.Dial so we can do http over tcp or unix. 360 // remoteAddr should be fully featured (eg. with tcp:// or unix://). 361 // An error will be returned in case of invalid remoteAddr. 362 func DefaultHTTPClient(remoteAddr string) (*http.Client, error) { 363 dialFn, err := makeHTTPDialer(remoteAddr) 364 if err != nil { 365 return nil, err 366 } 367 368 client := &http.Client{ 369 Transport: &http.Transport{ 370 // Set to true to prevent GZIP-bomb DoS attacks 371 DisableCompression: true, 372 Dial: dialFn, 373 }, 374 } 375 376 return client, nil 377 }