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