github.com/bytom/bytom@v1.1.2-0.20221014091027-bbcba3df6075/blockchain/rpc/rpc.go (about) 1 package rpc 2 3 import ( 4 "bytes" 5 "context" 6 "encoding/json" 7 "fmt" 8 "io" 9 "net/http" 10 "net/url" 11 "strings" 12 "time" 13 14 "github.com/bytom/bytom/errors" 15 "github.com/bytom/bytom/net/http/httperror" 16 "github.com/bytom/bytom/net/http/reqid" 17 ) 18 19 // Bytom-specific header fields 20 const ( 21 HeaderBlockchainID = "Blockchain-ID" 22 HeaderCoreID = "Bytom-Core-ID" 23 HeaderTimeout = "RPC-Timeout" 24 ) 25 26 // ErrWrongNetwork is returned when a peer's blockchain ID differs from 27 // the RPC client's blockchain ID. 28 var ErrWrongNetwork = errors.New("connected to a peer on a different network") 29 30 // A Client is a Bytom RPC client. It performs RPCs over HTTP using JSON 31 // request and responses. A Client must be configured with a secret token 32 // to authenticate with other Cores on the network. 33 type Client struct { 34 BaseURL string 35 AccessToken string 36 Username string 37 BuildTag string 38 BlockchainID string 39 CoreID string 40 41 // If set, Client is used for outgoing requests. 42 // TODO(kr): make this required (crash on nil) 43 Client *http.Client 44 } 45 46 func (c Client) userAgent() string { 47 return fmt.Sprintf("Bytom; process=%s; buildtag=%s; blockchainID=%s", 48 c.Username, c.BuildTag, c.BlockchainID) 49 } 50 51 // ErrStatusCode is an error returned when an rpc fails with a non-200 52 // response code. 53 type ErrStatusCode struct { 54 URL string 55 StatusCode int 56 ErrorData *httperror.Response 57 } 58 59 func (e ErrStatusCode) Error() string { 60 return fmt.Sprintf("Request to `%s` responded with %d %s", 61 e.URL, e.StatusCode, http.StatusText(e.StatusCode)) 62 } 63 64 // Call calls a remote procedure on another node, specified by the path. 65 func (c *Client) Call(ctx context.Context, path string, request, response interface{}) error { 66 r, err := c.CallRaw(ctx, path, request) 67 if err != nil { 68 return err 69 } 70 defer r.Close() 71 if response != nil { 72 decoder := json.NewDecoder(r) 73 decoder.UseNumber() 74 err = errors.Wrap(decoder.Decode(response)) 75 } 76 return err 77 } 78 79 // CallRaw calls a remote procedure on another node, specified by the path. It 80 // returns a io.ReadCloser of the raw response body. 81 func (c *Client) CallRaw(ctx context.Context, path string, request interface{}) (io.ReadCloser, error) { 82 u, err := url.Parse(c.BaseURL) 83 if err != nil { 84 return nil, errors.Wrap(err) 85 } 86 u.Path = path 87 88 var bodyReader io.Reader 89 if request != nil { 90 var jsonBody bytes.Buffer 91 if err := json.NewEncoder(&jsonBody).Encode(request); err != nil { 92 return nil, errors.Wrap(err) 93 } 94 bodyReader = &jsonBody 95 } 96 97 req, err := http.NewRequest("POST", u.String(), bodyReader) 98 if err != nil { 99 return nil, errors.Wrap(err) 100 } 101 102 if c.AccessToken != "" { 103 var username, password string 104 toks := strings.SplitN(c.AccessToken, ":", 2) 105 if len(toks) > 0 { 106 username = toks[0] 107 } 108 if len(toks) > 1 { 109 password = toks[1] 110 } 111 req.SetBasicAuth(username, password) 112 } 113 114 // Propagate our request ID so that we can trace a request across nodes. 115 req.Header.Add("Request-ID", reqid.FromContext(ctx)) 116 req.Header.Set("Content-Type", "application/json") 117 req.Header.Set("User-Agent", c.userAgent()) 118 req.Header.Set(HeaderBlockchainID, c.BlockchainID) 119 req.Header.Set(HeaderCoreID, c.CoreID) 120 121 // Propagate our deadline if we have one. 122 deadline, ok := ctx.Deadline() 123 if ok { 124 req.Header.Set(HeaderTimeout, deadline.Sub(time.Now()).String()) 125 } 126 127 client := c.Client 128 if client == nil { 129 client = http.DefaultClient 130 } 131 resp, err := client.Do(req.WithContext(ctx)) 132 if err != nil && ctx.Err() != nil { // check if it timed out 133 return nil, errors.Wrap(ctx.Err()) 134 } else if err != nil { 135 return nil, errors.Wrap(err) 136 } 137 138 if id := resp.Header.Get(HeaderBlockchainID); c.BlockchainID != "" && id != "" && c.BlockchainID != id { 139 resp.Body.Close() 140 return nil, errors.Wrap(ErrWrongNetwork) 141 } 142 143 if resp.StatusCode < 200 || resp.StatusCode >= 300 { 144 defer resp.Body.Close() 145 146 resErr := ErrStatusCode{ 147 URL: cleanedURLString(u), 148 StatusCode: resp.StatusCode, 149 } 150 151 // Attach formatted error message, if available 152 var errData httperror.Response 153 err := json.NewDecoder(resp.Body).Decode(&errData) 154 if err == nil && errData.ChainCode != "" { 155 resErr.ErrorData = &errData 156 } 157 158 return nil, resErr 159 } 160 161 return resp.Body, nil 162 } 163 164 func cleanedURLString(u *url.URL) string { 165 var dup url.URL = *u 166 dup.User = nil 167 return dup.String() 168 }