OSDN Git Service

Merge pull request #41 from Bytom/dev
[bytom/vapor.git] / blockchain / rpc / rpc.go
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/vapor/errors"
15         "github.com/vapor/net/http/httperror"
16         "github.com/vapor/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 }