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  }