rpc: various fixes/enhancements
rpc: be less restrictive on the request id rpc: improved documentation console: upgrade web3.js to version 0.16.0 rpc: cache http connections rpc: rename wsDomains parameter to wsOrigins
This commit is contained in:
14
rpc/doc.go
14
rpc/doc.go
@ -29,11 +29,23 @@ Methods that satisfy the following criteria are made available for remote access
|
||||
- method returned value(s) must be exported or builtin types
|
||||
|
||||
An example method:
|
||||
func (s *CalcService) Div(a, b int) (int, error)
|
||||
func (s *CalcService) Add(a, b int) (int, error)
|
||||
|
||||
When the returned error isn't nil the returned integer is ignored and the error is
|
||||
send back to the client. Otherwise the returned integer is send back to the client.
|
||||
|
||||
Optional arguments are supported by accepting pointer values as arguments. E.g.
|
||||
if we want to do the addition in an optional finite field we can accept a mod
|
||||
argument as pointer value.
|
||||
|
||||
func (s *CalService) Add(a, b int, mod *int) (int, error)
|
||||
|
||||
This RPC method can be called with 2 integers and a null value as third argument.
|
||||
In that case the mod argument will be nil. Or it can be called with 3 integers,
|
||||
in that case mod will be pointing to the given third argument. Since the optional
|
||||
argument is the last argument the RPC package will also accept 2 integers as
|
||||
arguments. It will pass the mod argument as nil to the RPC method.
|
||||
|
||||
The server offers the ServeCodec method which accepts a ServerCodec instance. It will
|
||||
read requests from the codec, process the request and sends the response back to the
|
||||
client using the codec. The server can execute requests concurrently. Responses
|
||||
|
20
rpc/http.go
20
rpc/http.go
@ -20,13 +20,12 @@ import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"io"
|
||||
|
||||
"github.com/rs/cors"
|
||||
)
|
||||
|
||||
@ -36,8 +35,9 @@ const (
|
||||
|
||||
// httpClient connects to a geth RPC server over HTTP.
|
||||
type httpClient struct {
|
||||
endpoint *url.URL // HTTP-RPC server endpoint
|
||||
lastRes []byte // HTTP requests are synchronous, store last response
|
||||
endpoint *url.URL // HTTP-RPC server endpoint
|
||||
httpClient http.Client // reuse connection
|
||||
lastRes []byte // HTTP requests are synchronous, store last response
|
||||
}
|
||||
|
||||
// NewHTTPClient create a new RPC clients that connection to a geth RPC server
|
||||
@ -57,30 +57,22 @@ func (client *httpClient) Send(msg interface{}) error {
|
||||
var err error
|
||||
|
||||
client.lastRes = nil
|
||||
|
||||
if body, err = json.Marshal(msg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
httpReq, err := http.NewRequest("POST", client.endpoint.String(), bytes.NewBuffer(body))
|
||||
resp, err := client.httpClient.Post(client.endpoint.String(), "application/json", bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
|
||||
httpClient := http.Client{}
|
||||
resp, err := httpClient.Do(httpReq)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
client.lastRes, err = ioutil.ReadAll(resp.Body)
|
||||
return err
|
||||
}
|
||||
|
||||
return fmt.Errorf("unable to handle request")
|
||||
return fmt.Errorf("request failed: %s", resp.Status)
|
||||
}
|
||||
|
||||
// Recv will try to deserialize the last received response into the given msg.
|
||||
|
@ -22,7 +22,7 @@ import (
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/microsoft/go-winio"
|
||||
winio "github.com/microsoft/go-winio"
|
||||
)
|
||||
|
||||
// ipcListen will create a named pipe on the given endpoint.
|
||||
|
@ -19,48 +19,15 @@ package rpc
|
||||
var (
|
||||
// Holds geth specific RPC extends which can be used to extend web3
|
||||
WEB3Extensions = map[string]string{
|
||||
"personal": Personal_JS,
|
||||
"txpool": TxPool_JS,
|
||||
"admin": Admin_JS,
|
||||
"eth": Eth_JS,
|
||||
"miner": Miner_JS,
|
||||
"debug": Debug_JS,
|
||||
"net": Net_JS,
|
||||
"txpool": TxPool_JS,
|
||||
"admin": Admin_JS,
|
||||
"eth": Eth_JS,
|
||||
"miner": Miner_JS,
|
||||
"debug": Debug_JS,
|
||||
"net": Net_JS,
|
||||
}
|
||||
)
|
||||
|
||||
const Personal_JS = `
|
||||
web3._extend({
|
||||
property: 'personal',
|
||||
methods:
|
||||
[
|
||||
new web3._extend.Method({
|
||||
name: 'newAccount',
|
||||
call: 'personal_newAccount',
|
||||
params: 1,
|
||||
outputFormatter: web3._extend.utils.toAddress
|
||||
}),
|
||||
new web3._extend.Method({
|
||||
name: 'unlockAccount',
|
||||
call: 'personal_unlockAccount',
|
||||
params: 3,
|
||||
}),
|
||||
new web3._extend.Method({
|
||||
name: 'lockAccount',
|
||||
call: 'personal_lockAccount',
|
||||
params: 1
|
||||
})
|
||||
],
|
||||
properties:
|
||||
[
|
||||
new web3._extend.Property({
|
||||
name: 'listAccounts',
|
||||
getter: 'personal_listAccounts'
|
||||
})
|
||||
]
|
||||
});
|
||||
`
|
||||
|
||||
const TxPool_JS = `
|
||||
web3._extend({
|
||||
property: 'txpool',
|
||||
@ -124,22 +91,22 @@ web3._extend({
|
||||
new web3._extend.Method({
|
||||
name: 'startRPC',
|
||||
call: 'admin_startRPC',
|
||||
params: 4
|
||||
params: 4,
|
||||
inputFormatter: [null, null, null, null]
|
||||
}),
|
||||
new web3._extend.Method({
|
||||
name: 'stopRPC',
|
||||
call: 'admin_stopRPC',
|
||||
params: 0
|
||||
call: 'admin_stopRPC'
|
||||
}),
|
||||
new web3._extend.Method({
|
||||
name: 'startWS',
|
||||
call: 'admin_startWS',
|
||||
params: 4
|
||||
params: 4,
|
||||
inputFormatter: [null, null, null, null]
|
||||
}),
|
||||
new web3._extend.Method({
|
||||
name: 'stopWS',
|
||||
call: 'admin_stopWS',
|
||||
params: 0
|
||||
call: 'admin_stopWS'
|
||||
}),
|
||||
new web3._extend.Method({
|
||||
name: 'setGlobalRegistrar',
|
||||
@ -219,7 +186,7 @@ web3._extend({
|
||||
name: 'sign',
|
||||
call: 'eth_sign',
|
||||
params: 2,
|
||||
inputFormatter: [web3._extend.utils.toAddress, null]
|
||||
inputFormatter: [web3._extend.formatters.inputAddressFormatter, null]
|
||||
}),
|
||||
new web3._extend.Method({
|
||||
name: 'resend',
|
||||
@ -414,19 +381,18 @@ web3._extend({
|
||||
new web3._extend.Method({
|
||||
name: 'start',
|
||||
call: 'miner_start',
|
||||
params: 1
|
||||
params: 1,
|
||||
inputFormatter: [null]
|
||||
}),
|
||||
new web3._extend.Method({
|
||||
name: 'stop',
|
||||
call: 'miner_stop',
|
||||
params: 1
|
||||
call: 'miner_stop'
|
||||
}),
|
||||
new web3._extend.Method({
|
||||
name: 'setEtherbase',
|
||||
call: 'miner_setEtherbase',
|
||||
params: 1,
|
||||
inputFormatter: [web3._extend.formatters.formatInputInt],
|
||||
outputFormatter: web3._extend.formatters.formatOutputBool
|
||||
inputFormatter: [web3._extend.formatters.inputAddressFormatter]
|
||||
}),
|
||||
new web3._extend.Method({
|
||||
name: 'setExtra',
|
||||
|
171
rpc/json.go
171
rpc/json.go
@ -21,6 +21,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
@ -40,14 +41,14 @@ const (
|
||||
type JSONRequest struct {
|
||||
Method string `json:"method"`
|
||||
Version string `json:"jsonrpc"`
|
||||
Id *int64 `json:"id,omitempty"`
|
||||
Id json.RawMessage `json:"id,omitempty"`
|
||||
Payload json.RawMessage `json:"params,omitempty"`
|
||||
}
|
||||
|
||||
// JSON-RPC response
|
||||
type JSONSuccessResponse struct {
|
||||
Version string `json:"jsonrpc"`
|
||||
Id int64 `json:"id"`
|
||||
Id interface{} `json:"id,omitempty"`
|
||||
Result interface{} `json:"result"`
|
||||
}
|
||||
|
||||
@ -60,9 +61,9 @@ type JSONError struct {
|
||||
|
||||
// JSON-RPC error response
|
||||
type JSONErrResponse struct {
|
||||
Version string `json:"jsonrpc"`
|
||||
Id *int64 `json:"id,omitempty"`
|
||||
Error JSONError `json:"error"`
|
||||
Version string `json:"jsonrpc"`
|
||||
Id interface{} `json:"id,omitempty"`
|
||||
Error JSONError `json:"error"`
|
||||
}
|
||||
|
||||
// JSON-RPC notification payload
|
||||
@ -78,16 +79,16 @@ type jsonNotification struct {
|
||||
Params jsonSubscription `json:"params"`
|
||||
}
|
||||
|
||||
// jsonCodec reads and writes JSON-RPC messages to the underlying connection. It also has support for parsing arguments
|
||||
// and serializing (result) objects.
|
||||
// jsonCodec reads and writes JSON-RPC messages to the underlying connection. It
|
||||
// also has support for parsing arguments and serializing (result) objects.
|
||||
type jsonCodec struct {
|
||||
closed chan interface{}
|
||||
closer sync.Once
|
||||
d *json.Decoder
|
||||
muEncoder sync.Mutex
|
||||
e *json.Encoder
|
||||
req JSONRequest
|
||||
rw io.ReadWriteCloser
|
||||
closer sync.Once // close closed channel once
|
||||
closed chan interface{} // closed on Close
|
||||
decMu sync.Mutex // guards d
|
||||
d *json.Decoder // decodes incoming requests
|
||||
encMu sync.Mutex // guards e
|
||||
e *json.Encoder // encodes responses
|
||||
rw io.ReadWriteCloser // connection
|
||||
}
|
||||
|
||||
// NewJSONCodec creates a new RPC server codec with support for JSON-RPC 2.0
|
||||
@ -109,9 +110,13 @@ func isBatch(msg json.RawMessage) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// ReadRequestHeaders will read new requests without parsing the arguments. It will return a collection of requests, an
|
||||
// indication if these requests are in batch form or an error when the incoming message could not be read/parsed.
|
||||
// ReadRequestHeaders will read new requests without parsing the arguments. It will
|
||||
// return a collection of requests, an indication if these requests are in batch
|
||||
// form or an error when the incoming message could not be read/parsed.
|
||||
func (c *jsonCodec) ReadRequestHeaders() ([]rpcRequest, bool, RPCError) {
|
||||
c.decMu.Lock()
|
||||
defer c.decMu.Unlock()
|
||||
|
||||
var incomingMsg json.RawMessage
|
||||
if err := c.d.Decode(&incomingMsg); err != nil {
|
||||
return nil, false, &invalidRequestError{err.Error()}
|
||||
@ -124,21 +129,38 @@ func (c *jsonCodec) ReadRequestHeaders() ([]rpcRequest, bool, RPCError) {
|
||||
return parseRequest(incomingMsg)
|
||||
}
|
||||
|
||||
// parseRequest will parse a single request from the given RawMessage. It will return the parsed request, an indication
|
||||
// if the request was a batch or an error when the request could not be parsed.
|
||||
// checkReqId returns an error when the given reqId isn't valid for RPC method calls.
|
||||
// valid id's are strings, numbers or null
|
||||
func checkReqId(reqId json.RawMessage) error {
|
||||
if len(reqId) == 0 {
|
||||
return fmt.Errorf("missing request id")
|
||||
}
|
||||
if _, err := strconv.ParseFloat(string(reqId), 64); err == nil {
|
||||
return nil
|
||||
}
|
||||
var str string
|
||||
if err := json.Unmarshal(reqId, &str); err == nil {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("invalid request id")
|
||||
}
|
||||
|
||||
// parseRequest will parse a single request from the given RawMessage. It will return
|
||||
// the parsed request, an indication if the request was a batch or an error when
|
||||
// the request could not be parsed.
|
||||
func parseRequest(incomingMsg json.RawMessage) ([]rpcRequest, bool, RPCError) {
|
||||
var in JSONRequest
|
||||
if err := json.Unmarshal(incomingMsg, &in); err != nil {
|
||||
return nil, false, &invalidMessageError{err.Error()}
|
||||
}
|
||||
|
||||
if in.Id == nil {
|
||||
return nil, false, &invalidMessageError{"Server cannot handle notifications"}
|
||||
if err := checkReqId(in.Id); err != nil {
|
||||
return nil, false, &invalidMessageError{err.Error()}
|
||||
}
|
||||
|
||||
// subscribe are special, they will always use `subscribeMethod` as service method
|
||||
// subscribe are special, they will always use `subscribeMethod` as first param in the payload
|
||||
if in.Method == subscribeMethod {
|
||||
reqs := []rpcRequest{rpcRequest{id: *in.Id, isPubSub: true}}
|
||||
reqs := []rpcRequest{rpcRequest{id: &in.Id, isPubSub: true}}
|
||||
if len(in.Payload) > 0 {
|
||||
// first param must be subscription name
|
||||
var subscribeMethod [1]string
|
||||
@ -156,7 +178,7 @@ func parseRequest(incomingMsg json.RawMessage) ([]rpcRequest, bool, RPCError) {
|
||||
}
|
||||
|
||||
if in.Method == unsubscribeMethod {
|
||||
return []rpcRequest{rpcRequest{id: *in.Id, isPubSub: true,
|
||||
return []rpcRequest{rpcRequest{id: &in.Id, isPubSub: true,
|
||||
method: unsubscribeMethod, params: in.Payload}}, false, nil
|
||||
}
|
||||
|
||||
@ -167,10 +189,10 @@ func parseRequest(incomingMsg json.RawMessage) ([]rpcRequest, bool, RPCError) {
|
||||
}
|
||||
|
||||
if len(in.Payload) == 0 {
|
||||
return []rpcRequest{rpcRequest{service: elems[0], method: elems[1], id: *in.Id}}, false, nil
|
||||
return []rpcRequest{rpcRequest{service: elems[0], method: elems[1], id: &in.Id}}, false, nil
|
||||
}
|
||||
|
||||
return []rpcRequest{rpcRequest{service: elems[0], method: elems[1], id: *in.Id, params: in.Payload}}, false, nil
|
||||
return []rpcRequest{rpcRequest{service: elems[0], method: elems[1], id: &in.Id, params: in.Payload}}, false, nil
|
||||
}
|
||||
|
||||
// parseBatchRequest will parse a batch request into a collection of requests from the given RawMessage, an indication
|
||||
@ -183,14 +205,17 @@ func parseBatchRequest(incomingMsg json.RawMessage) ([]rpcRequest, bool, RPCErro
|
||||
|
||||
requests := make([]rpcRequest, len(in))
|
||||
for i, r := range in {
|
||||
if r.Id == nil {
|
||||
return nil, true, &invalidMessageError{"Server cannot handle notifications"}
|
||||
if err := checkReqId(r.Id); err != nil {
|
||||
return nil, false, &invalidMessageError{err.Error()}
|
||||
}
|
||||
|
||||
// (un)subscribe are special, they will always use the same service.method
|
||||
id := &in[i].Id
|
||||
|
||||
// subscribe are special, they will always use `subscribeMethod` as first param in the payload
|
||||
if r.Method == subscribeMethod {
|
||||
requests[i] = rpcRequest{id: *r.Id, isPubSub: true}
|
||||
requests[i] = rpcRequest{id: id, isPubSub: true}
|
||||
if len(r.Payload) > 0 {
|
||||
// first param must be subscription name
|
||||
var subscribeMethod [1]string
|
||||
if err := json.Unmarshal(r.Payload, &subscribeMethod); err != nil {
|
||||
glog.V(logger.Debug).Infof("Unable to parse subscription method: %v\n", err)
|
||||
@ -207,7 +232,7 @@ func parseBatchRequest(incomingMsg json.RawMessage) ([]rpcRequest, bool, RPCErro
|
||||
}
|
||||
|
||||
if r.Method == unsubscribeMethod {
|
||||
requests[i] = rpcRequest{id: *r.Id, isPubSub: true, method: unsubscribeMethod, params: r.Payload}
|
||||
requests[i] = rpcRequest{id: id, isPubSub: true, method: unsubscribeMethod, params: r.Payload}
|
||||
continue
|
||||
}
|
||||
|
||||
@ -217,9 +242,9 @@ func parseBatchRequest(incomingMsg json.RawMessage) ([]rpcRequest, bool, RPCErro
|
||||
}
|
||||
|
||||
if len(r.Payload) == 0 {
|
||||
requests[i] = rpcRequest{service: elems[0], method: elems[1], id: *r.Id, params: nil}
|
||||
requests[i] = rpcRequest{service: elems[0], method: elems[1], id: id, params: nil}
|
||||
} else {
|
||||
requests[i] = rpcRequest{service: elems[0], method: elems[1], id: *r.Id, params: r.Payload}
|
||||
requests[i] = rpcRequest{service: elems[0], method: elems[1], id: id, params: r.Payload}
|
||||
}
|
||||
}
|
||||
|
||||
@ -236,58 +261,38 @@ func (c *jsonCodec) ParseRequestArguments(argTypes []reflect.Type, params interf
|
||||
}
|
||||
}
|
||||
|
||||
func countArguments(args json.RawMessage) (int, error) {
|
||||
var cnt []interface{}
|
||||
if err := json.Unmarshal(args, &cnt); err != nil {
|
||||
return -1, nil
|
||||
}
|
||||
return len(cnt), nil
|
||||
}
|
||||
|
||||
// parsePositionalArguments tries to parse the given args to an array of values with the given types. It returns the
|
||||
// parsed values or an error when the args could not be parsed.
|
||||
func parsePositionalArguments(args json.RawMessage, argTypes []reflect.Type) ([]reflect.Value, RPCError) {
|
||||
argValues := make([]reflect.Value, len(argTypes))
|
||||
params := make([]interface{}, len(argTypes))
|
||||
|
||||
n, err := countArguments(args)
|
||||
if err != nil {
|
||||
return nil, &invalidParamsError{err.Error()}
|
||||
}
|
||||
if n != len(argTypes) {
|
||||
return nil, &invalidParamsError{fmt.Sprintf("insufficient params, want %d have %d", len(argTypes), n)}
|
||||
}
|
||||
|
||||
for i, t := range argTypes {
|
||||
if t.Kind() == reflect.Ptr {
|
||||
// values must be pointers for the Unmarshal method, reflect.
|
||||
// Dereference otherwise reflect.New would create **SomeType
|
||||
argValues[i] = reflect.New(t.Elem())
|
||||
params[i] = argValues[i].Interface()
|
||||
|
||||
// when not specified blockNumbers are by default latest (-1)
|
||||
if blockNumber, ok := params[i].(*BlockNumber); ok {
|
||||
*blockNumber = BlockNumber(-1)
|
||||
}
|
||||
} else {
|
||||
argValues[i] = reflect.New(t)
|
||||
params[i] = argValues[i].Interface()
|
||||
|
||||
// when not specified blockNumbers are by default latest (-1)
|
||||
if blockNumber, ok := params[i].(*BlockNumber); ok {
|
||||
*blockNumber = BlockNumber(-1)
|
||||
}
|
||||
}
|
||||
// parsePositionalArguments tries to parse the given args to an array of values with the given types.
|
||||
// It returns the parsed values or an error when the args could not be parsed. Missing optional arguments
|
||||
// are returned as reflect.Zero values.
|
||||
func parsePositionalArguments(args json.RawMessage, callbackArgs []reflect.Type) ([]reflect.Value, RPCError) {
|
||||
params := make([]interface{}, 0, len(callbackArgs))
|
||||
for _, t := range callbackArgs {
|
||||
params = append(params, reflect.New(t).Interface())
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(args, ¶ms); err != nil {
|
||||
return nil, &invalidParamsError{err.Error()}
|
||||
}
|
||||
|
||||
// Convert pointers back to values where necessary
|
||||
for i, a := range argValues {
|
||||
if a.Kind() != argTypes[i].Kind() {
|
||||
argValues[i] = reflect.Indirect(argValues[i])
|
||||
if len(params) > len(callbackArgs) {
|
||||
return nil, &invalidParamsError{fmt.Sprintf("too many params, want %d got %d", len(callbackArgs), len(params))}
|
||||
}
|
||||
|
||||
// assume missing params are null values
|
||||
for i := len(params); i < len(callbackArgs); i++ {
|
||||
params = append(params, nil)
|
||||
}
|
||||
|
||||
argValues := make([]reflect.Value, len(params))
|
||||
for i, p := range params {
|
||||
// verify that JSON null values are only supplied for optional arguments (ptr types)
|
||||
if p == nil && callbackArgs[i].Kind() != reflect.Ptr {
|
||||
return nil, &invalidParamsError{fmt.Sprintf("invalid or missing value for params[%d]", i)}
|
||||
}
|
||||
if p == nil {
|
||||
argValues[i] = reflect.Zero(callbackArgs[i])
|
||||
} else { // deref pointers values creates previously with reflect.New
|
||||
argValues[i] = reflect.ValueOf(p).Elem()
|
||||
}
|
||||
}
|
||||
|
||||
@ -295,7 +300,7 @@ func parsePositionalArguments(args json.RawMessage, argTypes []reflect.Type) ([]
|
||||
}
|
||||
|
||||
// CreateResponse will create a JSON-RPC success response with the given id and reply as result.
|
||||
func (c *jsonCodec) CreateResponse(id int64, reply interface{}) interface{} {
|
||||
func (c *jsonCodec) CreateResponse(id interface{}, reply interface{}) interface{} {
|
||||
if isHexNum(reflect.TypeOf(reply)) {
|
||||
return &JSONSuccessResponse{Version: jsonRPCVersion, Id: id, Result: fmt.Sprintf(`%#x`, reply)}
|
||||
}
|
||||
@ -303,13 +308,13 @@ func (c *jsonCodec) CreateResponse(id int64, reply interface{}) interface{} {
|
||||
}
|
||||
|
||||
// CreateErrorResponse will create a JSON-RPC error response with the given id and error.
|
||||
func (c *jsonCodec) CreateErrorResponse(id *int64, err RPCError) interface{} {
|
||||
func (c *jsonCodec) CreateErrorResponse(id interface{}, err RPCError) interface{} {
|
||||
return &JSONErrResponse{Version: jsonRPCVersion, Id: id, Error: JSONError{Code: err.Code(), Message: err.Error()}}
|
||||
}
|
||||
|
||||
// CreateErrorResponseWithInfo will create a JSON-RPC error response with the given id and error.
|
||||
// info is optional and contains additional information about the error. When an empty string is passed it is ignored.
|
||||
func (c *jsonCodec) CreateErrorResponseWithInfo(id *int64, err RPCError, info interface{}) interface{} {
|
||||
func (c *jsonCodec) CreateErrorResponseWithInfo(id interface{}, err RPCError, info interface{}) interface{} {
|
||||
return &JSONErrResponse{Version: jsonRPCVersion, Id: id,
|
||||
Error: JSONError{Code: err.Code(), Message: err.Error(), Data: info}}
|
||||
}
|
||||
@ -327,8 +332,8 @@ func (c *jsonCodec) CreateNotification(subid string, event interface{}) interfac
|
||||
|
||||
// Write message to client
|
||||
func (c *jsonCodec) Write(res interface{}) error {
|
||||
c.muEncoder.Lock()
|
||||
defer c.muEncoder.Unlock()
|
||||
c.encMu.Lock()
|
||||
defer c.encMu.Unlock()
|
||||
|
||||
return c.e.Encode(res)
|
||||
}
|
||||
|
@ -3,7 +3,9 @@ package rpc
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@ -51,8 +53,16 @@ func TestJSONRequestParsing(t *testing.T) {
|
||||
t.Fatalf("Expected method 'Add' but got '%s'", requests[0].method)
|
||||
}
|
||||
|
||||
if requests[0].id != 1234 {
|
||||
t.Fatalf("Expected id 1234 but got %d", requests[0].id)
|
||||
if rawId, ok := requests[0].id.(*json.RawMessage); ok {
|
||||
id, e := strconv.ParseInt(string(*rawId), 0, 64)
|
||||
if e != nil {
|
||||
t.Fatalf("%v", e)
|
||||
}
|
||||
if id != 1234 {
|
||||
t.Fatalf("Expected id 1234 but got %s", id)
|
||||
}
|
||||
} else {
|
||||
t.Fatalf("invalid request, expected *json.RawMesage got %T", requests[0].id)
|
||||
}
|
||||
|
||||
var arg int
|
||||
@ -71,3 +81,82 @@ func TestJSONRequestParsing(t *testing.T) {
|
||||
t.Fatalf("expected %d == 11 && %d == 22", v[0].Int(), v[1].Int())
|
||||
}
|
||||
}
|
||||
|
||||
func TestJSONRequestParamsParsing(t *testing.T) {
|
||||
|
||||
var (
|
||||
stringT = reflect.TypeOf("")
|
||||
intT = reflect.TypeOf(0)
|
||||
intPtrT = reflect.TypeOf(new(int))
|
||||
|
||||
stringV = reflect.ValueOf("abc")
|
||||
i = 1
|
||||
intV = reflect.ValueOf(i)
|
||||
intPtrV = reflect.ValueOf(&i)
|
||||
)
|
||||
|
||||
var validTests = []struct {
|
||||
input string
|
||||
argTypes []reflect.Type
|
||||
expected []reflect.Value
|
||||
}{
|
||||
{`[]`, []reflect.Type{}, []reflect.Value{}},
|
||||
{`[]`, []reflect.Type{intPtrT}, []reflect.Value{intPtrV}},
|
||||
{`[1]`, []reflect.Type{intT}, []reflect.Value{intV}},
|
||||
{`[1,"abc"]`, []reflect.Type{intT, stringT}, []reflect.Value{intV, stringV}},
|
||||
{`[null]`, []reflect.Type{intPtrT}, []reflect.Value{intPtrV}},
|
||||
{`[null,"abc"]`, []reflect.Type{intPtrT, stringT, intPtrT}, []reflect.Value{intPtrV, stringV, intPtrV}},
|
||||
{`[null,"abc",null]`, []reflect.Type{intPtrT, stringT, intPtrT}, []reflect.Value{intPtrV, stringV, intPtrV}},
|
||||
}
|
||||
|
||||
codec := jsonCodec{}
|
||||
|
||||
for _, test := range validTests {
|
||||
params := (json.RawMessage)([]byte(test.input))
|
||||
args, err := codec.ParseRequestArguments(test.argTypes, params)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var match []interface{}
|
||||
json.Unmarshal([]byte(test.input), &match)
|
||||
|
||||
if len(args) != len(test.argTypes) {
|
||||
t.Fatalf("expected %d parsed args, got %d", len(test.argTypes), len(args))
|
||||
}
|
||||
|
||||
for i, arg := range args {
|
||||
expected := test.expected[i]
|
||||
|
||||
if arg.Kind() != expected.Kind() {
|
||||
t.Errorf("expected type for param %d in %s", i, test.input)
|
||||
}
|
||||
|
||||
if arg.Kind() == reflect.Int && arg.Int() != expected.Int() {
|
||||
t.Errorf("expected int(%d), got int(%d) in %s", expected.Int(), arg.Int(), test.input)
|
||||
}
|
||||
|
||||
if arg.Kind() == reflect.String && arg.String() != expected.String() {
|
||||
t.Errorf("expected string(%s), got string(%s) in %s", expected.String(), arg.String(), test.input)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var invalidTests = []struct {
|
||||
input string
|
||||
argTypes []reflect.Type
|
||||
}{
|
||||
{`[]`, []reflect.Type{intT}},
|
||||
{`[null]`, []reflect.Type{intT}},
|
||||
{`[1]`, []reflect.Type{stringT}},
|
||||
{`[1,2]`, []reflect.Type{stringT}},
|
||||
{`["abc", null]`, []reflect.Type{stringT, intT}},
|
||||
}
|
||||
|
||||
for i, test := range invalidTests {
|
||||
if _, err := codec.ParseRequestArguments(test.argTypes, test.input); err == nil {
|
||||
t.Errorf("expected test %d - %s to fail", i, test.input)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -18,10 +18,9 @@ package rpc
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
@ -69,10 +68,6 @@ func (s *Service) Subscription(ctx context.Context) (Subscription, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s *Service) SubsriptionWithArgs(ctx context.Context, a, b int) (Subscription, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func TestServerRegisterName(t *testing.T) {
|
||||
server := NewServer()
|
||||
service := new(Service)
|
||||
@ -94,182 +89,67 @@ func TestServerRegisterName(t *testing.T) {
|
||||
t.Errorf("Expected 4 callbacks for service 'calc', got %d", len(svc.callbacks))
|
||||
}
|
||||
|
||||
if len(svc.subscriptions) != 2 {
|
||||
t.Errorf("Expected 2 subscriptions for service 'calc', got %d", len(svc.subscriptions))
|
||||
if len(svc.subscriptions) != 1 {
|
||||
t.Errorf("Expected 1 subscription for service 'calc', got %d", len(svc.subscriptions))
|
||||
}
|
||||
}
|
||||
|
||||
// dummy codec used for testing RPC method execution
|
||||
type ServerTestCodec struct {
|
||||
counter int
|
||||
input []byte
|
||||
output string
|
||||
closer chan interface{}
|
||||
}
|
||||
func testServerMethodExecution(t *testing.T, method string) {
|
||||
server := NewServer()
|
||||
service := new(Service)
|
||||
|
||||
func (c *ServerTestCodec) ReadRequestHeaders() ([]rpcRequest, bool, RPCError) {
|
||||
c.counter += 1
|
||||
|
||||
if c.counter == 1 {
|
||||
var req JSONRequest
|
||||
json.Unmarshal(c.input, &req)
|
||||
return []rpcRequest{rpcRequest{id: *req.Id, isPubSub: false, service: "test", method: req.Method, params: req.Payload}}, false, nil
|
||||
if err := server.RegisterName("test", service); err != nil {
|
||||
t.Fatalf("%v", err)
|
||||
}
|
||||
|
||||
// requests are executes in parallel, wait a bit before returning an error so that the previous request has time to
|
||||
// be executed
|
||||
timer := time.NewTimer(time.Duration(2) * time.Second)
|
||||
<-timer.C
|
||||
|
||||
return nil, false, &invalidRequestError{"connection closed"}
|
||||
}
|
||||
|
||||
func (c *ServerTestCodec) ParseRequestArguments(argTypes []reflect.Type, payload interface{}) ([]reflect.Value, RPCError) {
|
||||
|
||||
args, _ := payload.(json.RawMessage)
|
||||
|
||||
argValues := make([]reflect.Value, len(argTypes))
|
||||
params := make([]interface{}, len(argTypes))
|
||||
|
||||
n, err := countArguments(args)
|
||||
if err != nil {
|
||||
return nil, &invalidParamsError{err.Error()}
|
||||
}
|
||||
if n != len(argTypes) {
|
||||
return nil, &invalidParamsError{fmt.Sprintf("insufficient params, want %d have %d", len(argTypes), n)}
|
||||
stringArg := "string arg"
|
||||
intArg := 1122
|
||||
argsArg := &Args{"abcde"}
|
||||
params := []interface{}{stringArg, intArg, argsArg}
|
||||
|
||||
request := map[string]interface{}{
|
||||
"id": 12345,
|
||||
"method": "test_" + method,
|
||||
"version": "2.0",
|
||||
"params": params,
|
||||
}
|
||||
|
||||
for i, t := range argTypes {
|
||||
if t.Kind() == reflect.Ptr {
|
||||
// values must be pointers for the Unmarshal method, reflect.
|
||||
// Dereference otherwise reflect.New would create **SomeType
|
||||
argValues[i] = reflect.New(t.Elem())
|
||||
params[i] = argValues[i].Interface()
|
||||
clientConn, serverConn := net.Pipe()
|
||||
defer clientConn.Close()
|
||||
|
||||
// when not specified blockNumbers are by default latest (-1)
|
||||
if blockNumber, ok := params[i].(*BlockNumber); ok {
|
||||
*blockNumber = BlockNumber(-1)
|
||||
}
|
||||
} else {
|
||||
argValues[i] = reflect.New(t)
|
||||
params[i] = argValues[i].Interface()
|
||||
go server.ServeCodec(NewJSONCodec(serverConn), OptionMethodInvocation)
|
||||
|
||||
// when not specified blockNumbers are by default latest (-1)
|
||||
if blockNumber, ok := params[i].(*BlockNumber); ok {
|
||||
*blockNumber = BlockNumber(-1)
|
||||
}
|
||||
out := json.NewEncoder(clientConn)
|
||||
in := json.NewDecoder(clientConn)
|
||||
|
||||
if err := out.Encode(request); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
response := JSONSuccessResponse{Result: &Result{}}
|
||||
if err := in.Decode(&response); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if result, ok := response.Result.(*Result); ok {
|
||||
if result.String != stringArg {
|
||||
t.Errorf("expected %s, got : %s\n", stringArg, result.String)
|
||||
}
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(args, ¶ms); err != nil {
|
||||
return nil, &invalidParamsError{err.Error()}
|
||||
}
|
||||
|
||||
// Convert pointers back to values where necessary
|
||||
for i, a := range argValues {
|
||||
if a.Kind() != argTypes[i].Kind() {
|
||||
argValues[i] = reflect.Indirect(argValues[i])
|
||||
if result.Int != intArg {
|
||||
t.Errorf("expected %d, got %d\n", intArg, result.Int)
|
||||
}
|
||||
}
|
||||
|
||||
return argValues, nil
|
||||
}
|
||||
|
||||
func (c *ServerTestCodec) CreateResponse(id int64, reply interface{}) interface{} {
|
||||
return &JSONSuccessResponse{Version: jsonRPCVersion, Id: id, Result: reply}
|
||||
}
|
||||
|
||||
func (c *ServerTestCodec) CreateErrorResponse(id *int64, err RPCError) interface{} {
|
||||
return &JSONErrResponse{Version: jsonRPCVersion, Id: id, Error: JSONError{Code: err.Code(), Message: err.Error()}}
|
||||
}
|
||||
|
||||
func (c *ServerTestCodec) CreateErrorResponseWithInfo(id *int64, err RPCError, info interface{}) interface{} {
|
||||
return &JSONErrResponse{Version: jsonRPCVersion, Id: id,
|
||||
Error: JSONError{Code: err.Code(), Message: err.Error(), Data: info}}
|
||||
}
|
||||
|
||||
func (c *ServerTestCodec) CreateNotification(subid string, event interface{}) interface{} {
|
||||
return &jsonNotification{Version: jsonRPCVersion, Method: notificationMethod,
|
||||
Params: jsonSubscription{Subscription: subid, Result: event}}
|
||||
}
|
||||
|
||||
func (c *ServerTestCodec) Write(msg interface{}) error {
|
||||
if len(c.output) == 0 { // only capture first response
|
||||
if o, err := json.Marshal(msg); err != nil {
|
||||
return err
|
||||
} else {
|
||||
c.output = string(o)
|
||||
if !reflect.DeepEqual(result.Args, argsArg) {
|
||||
t.Errorf("expected %v, got %v\n", argsArg, result)
|
||||
}
|
||||
} else {
|
||||
t.Fatalf("invalid response: expected *Result - got: %T", response.Result)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *ServerTestCodec) Close() {
|
||||
close(c.closer)
|
||||
}
|
||||
|
||||
func (c *ServerTestCodec) Closed() <-chan interface{} {
|
||||
return c.closer
|
||||
}
|
||||
|
||||
func TestServerMethodExecution(t *testing.T) {
|
||||
server := NewServer()
|
||||
service := new(Service)
|
||||
|
||||
if err := server.RegisterName("test", service); err != nil {
|
||||
t.Fatalf("%v", err)
|
||||
}
|
||||
|
||||
id := int64(12345)
|
||||
req := JSONRequest{
|
||||
Method: "echo",
|
||||
Version: "2.0",
|
||||
Id: &id,
|
||||
}
|
||||
args := []interface{}{"string arg", 1122, &Args{"qwerty"}}
|
||||
req.Payload, _ = json.Marshal(&args)
|
||||
|
||||
input, _ := json.Marshal(&req)
|
||||
codec := &ServerTestCodec{input: input, closer: make(chan interface{})}
|
||||
go server.ServeCodec(codec, OptionMethodInvocation)
|
||||
|
||||
<-codec.closer
|
||||
|
||||
expected := `{"jsonrpc":"2.0","id":12345,"result":{"String":"string arg","Int":1122,"Args":{"S":"qwerty"}}}`
|
||||
|
||||
if expected != codec.output {
|
||||
t.Fatalf("expected %s, got %s\n", expected, codec.output)
|
||||
}
|
||||
testServerMethodExecution(t, "echo")
|
||||
}
|
||||
|
||||
func TestServerMethodWithCtx(t *testing.T) {
|
||||
server := NewServer()
|
||||
service := new(Service)
|
||||
|
||||
if err := server.RegisterName("test", service); err != nil {
|
||||
t.Fatalf("%v", err)
|
||||
}
|
||||
|
||||
id := int64(12345)
|
||||
req := JSONRequest{
|
||||
Method: "echoWithCtx",
|
||||
Version: "2.0",
|
||||
Id: &id,
|
||||
}
|
||||
args := []interface{}{"string arg", 1122, &Args{"qwerty"}}
|
||||
req.Payload, _ = json.Marshal(&args)
|
||||
|
||||
input, _ := json.Marshal(&req)
|
||||
codec := &ServerTestCodec{input: input, closer: make(chan interface{})}
|
||||
go server.ServeCodec(codec, OptionMethodInvocation)
|
||||
|
||||
<-codec.closer
|
||||
|
||||
expected := `{"jsonrpc":"2.0","id":12345,"result":{"String":"string arg","Int":1122,"Args":{"S":"qwerty"}}}`
|
||||
|
||||
if expected != codec.output {
|
||||
t.Fatalf("expected %s, got %s\n", expected, codec.output)
|
||||
}
|
||||
testServerMethodExecution(t, "echoWithCtx")
|
||||
}
|
||||
|
51
rpc/types.go
51
rpc/types.go
@ -56,7 +56,7 @@ type service struct {
|
||||
|
||||
// serverRequest is an incoming request
|
||||
type serverRequest struct {
|
||||
id int64
|
||||
id interface{}
|
||||
svcname string
|
||||
rcvr reflect.Value
|
||||
callb *callback
|
||||
@ -85,7 +85,7 @@ type Server struct {
|
||||
type rpcRequest struct {
|
||||
service string
|
||||
method string
|
||||
id int64
|
||||
id interface{}
|
||||
isPubSub bool
|
||||
params interface{}
|
||||
}
|
||||
@ -106,12 +106,12 @@ type ServerCodec interface {
|
||||
ReadRequestHeaders() ([]rpcRequest, bool, RPCError)
|
||||
// Parse request argument to the given types
|
||||
ParseRequestArguments([]reflect.Type, interface{}) ([]reflect.Value, RPCError)
|
||||
// Assemble success response
|
||||
CreateResponse(int64, interface{}) interface{}
|
||||
// Assemble error response
|
||||
CreateErrorResponse(*int64, RPCError) interface{}
|
||||
// Assemble success response, expects response id and payload
|
||||
CreateResponse(interface{}, interface{}) interface{}
|
||||
// Assemble error response, expects response id and error
|
||||
CreateErrorResponse(interface{}, RPCError) interface{}
|
||||
// Assemble error response with extra information about the error through info
|
||||
CreateErrorResponseWithInfo(id *int64, err RPCError, info interface{}) interface{}
|
||||
CreateErrorResponseWithInfo(id interface{}, err RPCError, info interface{}) interface{}
|
||||
// Create notification response
|
||||
CreateNotification(string, interface{}) interface{}
|
||||
// Write msg to client.
|
||||
@ -207,43 +207,6 @@ func (h *HexNumber) BigInt() *big.Int {
|
||||
return (*big.Int)(h)
|
||||
}
|
||||
|
||||
type Number int64
|
||||
|
||||
func (n *Number) UnmarshalJSON(data []byte) error {
|
||||
input := strings.TrimSpace(string(data))
|
||||
|
||||
if len(input) >= 2 && input[0] == '"' && input[len(input)-1] == '"' {
|
||||
input = input[1 : len(input)-1]
|
||||
}
|
||||
|
||||
if len(input) == 0 {
|
||||
*n = Number(latestBlockNumber.Int64())
|
||||
return nil
|
||||
}
|
||||
|
||||
in := new(big.Int)
|
||||
_, ok := in.SetString(input, 0)
|
||||
|
||||
if !ok { // test if user supplied string tag
|
||||
return fmt.Errorf(`invalid number %s`, data)
|
||||
}
|
||||
|
||||
if in.Cmp(earliestBlockNumber) >= 0 && in.Cmp(maxBlockNumber) <= 0 {
|
||||
*n = Number(in.Int64())
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("blocknumber not in range [%d, %d]", earliestBlockNumber, maxBlockNumber)
|
||||
}
|
||||
|
||||
func (n *Number) Int64() int64 {
|
||||
return *(*int64)(n)
|
||||
}
|
||||
|
||||
func (n *Number) BigInt() *big.Int {
|
||||
return big.NewInt(n.Int64())
|
||||
}
|
||||
|
||||
var (
|
||||
pendingBlockNumber = big.NewInt(-2)
|
||||
latestBlockNumber = big.NewInt(-1)
|
||||
|
@ -232,7 +232,7 @@ func newSubscriptionID() (string, error) {
|
||||
// on which the given client connects.
|
||||
func SupportedModules(client Client) (map[string]string, error) {
|
||||
req := JSONRequest{
|
||||
Id: new(int64),
|
||||
Id: []byte("1"),
|
||||
Version: "2.0",
|
||||
Method: "rpc_modules",
|
||||
}
|
||||
|
@ -88,10 +88,10 @@ func wsHandshakeValidator(allowedOrigins []string) func(*websocket.Config, *http
|
||||
}
|
||||
|
||||
// NewWSServer creates a new websocket RPC server around an API provider.
|
||||
func NewWSServer(cors string, handler *Server) *http.Server {
|
||||
func NewWSServer(allowedOrigins string, handler *Server) *http.Server {
|
||||
return &http.Server{
|
||||
Handler: websocket.Server{
|
||||
Handshake: wsHandshakeValidator(strings.Split(cors, ",")),
|
||||
Handshake: wsHandshakeValidator(strings.Split(allowedOrigins, ",")),
|
||||
Handler: func(conn *websocket.Conn) {
|
||||
handler.ServeCodec(NewJSONCodec(&wsReaderWriterCloser{conn}),
|
||||
OptionMethodInvocation|OptionSubscriptions)
|
||||
|
Reference in New Issue
Block a user