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