aboutsummaryrefslogtreecommitdiffstats
path: root/rpc
diff options
context:
space:
mode:
authorBas van Kervel <bas@ethdev.com>2016-03-14 16:38:54 +0800
committerBas van Kervel <bas@ethdev.com>2016-04-12 17:02:39 +0800
commitaa9fff3e68b1def0a9a22009c233150bf9ba481f (patch)
tree926c241574d6d80dfe4ffd6d2e447a9f7f84dc8b /rpc
parent7e02105672cda92889a78db864a5701d78f45eb2 (diff)
downloadgo-tangerine-aa9fff3e68b1def0a9a22009c233150bf9ba481f.tar
go-tangerine-aa9fff3e68b1def0a9a22009c233150bf9ba481f.tar.gz
go-tangerine-aa9fff3e68b1def0a9a22009c233150bf9ba481f.tar.bz2
go-tangerine-aa9fff3e68b1def0a9a22009c233150bf9ba481f.tar.lz
go-tangerine-aa9fff3e68b1def0a9a22009c233150bf9ba481f.tar.xz
go-tangerine-aa9fff3e68b1def0a9a22009c233150bf9ba481f.tar.zst
go-tangerine-aa9fff3e68b1def0a9a22009c233150bf9ba481f.zip
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
Diffstat (limited to 'rpc')
-rw-r--r--rpc/doc.go14
-rw-r--r--rpc/http.go20
-rw-r--r--rpc/ipc_windows.go2
-rw-r--r--rpc/javascript.go68
-rw-r--r--rpc/json.go165
-rw-r--r--rpc/json_test.go93
-rw-r--r--rpc/server_test.go198
-rw-r--r--rpc/types.go51
-rw-r--r--rpc/utils.go2
-rw-r--r--rpc/websocket.go4
10 files changed, 262 insertions, 355 deletions
diff --git a/rpc/doc.go b/rpc/doc.go
index c9dba3270..77202634f 100644
--- a/rpc/doc.go
+++ b/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
diff --git a/rpc/http.go b/rpc/http.go
index dd1ec2c01..9283ce0ec 100644
--- a/rpc/http.go
+++ b/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.
diff --git a/rpc/ipc_windows.go b/rpc/ipc_windows.go
index c5f69589e..8762cdb0d 100644
--- a/rpc/ipc_windows.go
+++ b/rpc/ipc_windows.go
@@ -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.
diff --git a/rpc/javascript.go b/rpc/javascript.go
index c4fa80c0b..64af8968f 100644
--- a/rpc/javascript.go
+++ b/rpc/javascript.go
@@ -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',
diff --git a/rpc/json.go b/rpc/json.go
index a0bfcac04..8a3bea2ee 100644
--- a/rpc/json.go
+++ b/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
+// 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())
}
- 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 {
+ if err := json.Unmarshal(args, &params); 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)
- }
- }
+ if len(params) > len(callbackArgs) {
+ return nil, &invalidParamsError{fmt.Sprintf("too many params, want %d got %d", len(callbackArgs), len(params))}
}
- if err := json.Unmarshal(args, &params); err != nil {
- return nil, &invalidParamsError{err.Error()}
+ // assume missing params are null values
+ for i := len(params); i < len(callbackArgs); i++ {
+ params = append(params, nil)
}
- // Convert pointers back to values where necessary
- for i, a := range argValues {
- if a.Kind() != argTypes[i].Kind() {
- argValues[i] = reflect.Indirect(argValues[i])
+ 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)
}
diff --git a/rpc/json_test.go b/rpc/json_test.go
index 39aae1f54..418b0261d 100644
--- a/rpc/json_test.go
+++ b/rpc/json_test.go
@@ -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)
+ }
+ }
+}
diff --git a/rpc/server_test.go b/rpc/server_test.go
index c60db38df..de47e1afd 100644
--- a/rpc/server_test.go
+++ b/rpc/server_test.go
@@ -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 (c *ServerTestCodec) ReadRequestHeaders() ([]rpcRequest, bool, RPCError) {
- c.counter += 1
+func testServerMethodExecution(t *testing.T, method string) {
+ server := NewServer()
+ service := new(Service)
- 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
+ stringArg := "string arg"
+ intArg := 1122
+ argsArg := &Args{"abcde"}
+ params := []interface{}{stringArg, intArg, argsArg}
- return nil, false, &invalidRequestError{"connection closed"}
-}
+ request := map[string]interface{}{
+ "id": 12345,
+ "method": "test_" + method,
+ "version": "2.0",
+ "params": params,
+ }
-func (c *ServerTestCodec) ParseRequestArguments(argTypes []reflect.Type, payload interface{}) ([]reflect.Value, RPCError) {
+ clientConn, serverConn := net.Pipe()
+ defer clientConn.Close()
- args, _ := payload.(json.RawMessage)
+ go server.ServeCodec(NewJSONCodec(serverConn), OptionMethodInvocation)
- argValues := make([]reflect.Value, len(argTypes))
- params := make([]interface{}, len(argTypes))
+ out := json.NewEncoder(clientConn)
+ in := json.NewDecoder(clientConn)
- n, err := countArguments(args)
- if err != nil {
- return nil, &invalidParamsError{err.Error()}
+ if err := out.Encode(request); err != nil {
+ t.Fatal(err)
}
- if n != len(argTypes) {
- return nil, &invalidParamsError{fmt.Sprintf("insufficient params, want %d have %d", len(argTypes), n)}
+ response := JSONSuccessResponse{Result: &Result{}}
+ if err := in.Decode(&response); err != nil {
+ t.Fatal(err)
}
- 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)
- }
+ 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, &params); 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")
}
diff --git a/rpc/types.go b/rpc/types.go
index 596fdf264..a1f36fbd2 100644
--- a/rpc/types.go
+++ b/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)
diff --git a/rpc/utils.go b/rpc/utils.go
index d43c50495..86938e9b3 100644
--- a/rpc/utils.go
+++ b/rpc/utils.go
@@ -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",
}
diff --git a/rpc/websocket.go b/rpc/websocket.go
index 499eedabe..1303f98db 100644
--- a/rpc/websocket.go
+++ b/rpc/websocket.go
@@ -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)