OSDN Git Service

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