From b03facd828da4fe8ec325a57a89d72510308eeb7 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Tue, 7 Mar 2017 18:34:13 +0400 Subject: [PATCH 01/38] add Dockerfile --- Dockerfile | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 Dockerfile diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..c23c911ce --- /dev/null +++ b/Dockerfile @@ -0,0 +1,12 @@ +FROM golang:latest + +RUN mkdir -p /go/src/github.com/tendermint/go-rpc +WORKDIR /go/src/github.com/tendermint/go-rpc + +COPY Makefile /go/src/github.com/tendermint/go-rpc/ +# COPY glide.yaml /go/src/github.com/tendermint/go-rpc/ +# COPY glide.lock /go/src/github.com/tendermint/go-rpc/ + +COPY . /go/src/github.com/tendermint/go-rpc + +RUN make get_deps From e1d5873bdfc95728971a38297ec3150a556397f9 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Tue, 7 Mar 2017 18:34:54 +0400 Subject: [PATCH 02/38] support key-value params in JSONRPC (Refs #1) More changes: - remove Client interface (reason: empty) - introduce HTTPClient interface, which can be used for both ClientURI and ClientJSONRPC clients (so our users don't have to create their own) (Refs #8) - rename integration tests script to `integration_test.sh` - do not update deps on `get_deps` --- Makefile | 14 +++- README.md | 20 ++--- client/http_client.go | 32 ++++---- client/ws_client.go | 12 +-- rpc_test.go | 16 ++-- server/handlers.go | 103 ++++++++++++++++---------- test/data.json | 11 ++- test/{test.sh => integration_test.sh} | 31 ++++---- test/main.go | 6 +- types/types.go | 14 ++-- 10 files changed, 147 insertions(+), 112 deletions(-) rename test/{test.sh => integration_test.sh} (65%) diff --git a/Makefile b/Makefile index 3b005ea36..e17fa06ac 100644 --- a/Makefile +++ b/Makefile @@ -1,9 +1,15 @@ -.PHONY: all test get_deps +PACKAGES=$(shell go list ./...) all: test -test: - bash ./test/test.sh +test: + @echo "--> Running go test --race" + @go test --race $(PACKAGES) + @echo "--> Running integration tests" + @bash ./test/integration_test.sh get_deps: - go get -t -u github.com/tendermint/go-rpc/... + @echo "--> Running go get" + @go get -v -d $(PACKAGES) + +.PHONY: all test get_deps diff --git a/README.md b/README.md index 1a91fb693..4da0e5634 100644 --- a/README.md +++ b/README.md @@ -32,16 +32,16 @@ As a POST request, we use JSONRPC. For instance, the same request would have thi ``` { - "jsonrpc":"2.0", - "id":"anything", - "method":"hello_world", - "params":["my_world", 5] + "jsonrpc": "2.0", + "id": "anything", + "method": "hello_world", + "params": { + "name": "my_world", + "num": 5 + } } ``` -Note the `params` does not currently support key-value pairs (https://github.com/tendermint/go-rpc/issues/1), so order matters (you can get the order from making a -GET request to `/`) - With the above saved in file `data.json`, we can make the request with ``` @@ -50,8 +50,8 @@ curl --data @data.json http://localhost:8008 ## WebSocket (JSONRPC) -All requests are exposed over websocket in the same form as the POST JSONRPC. -Websocket connections are available at their own endpoint, typically `/websocket`, +All requests are exposed over websocket in the same form as the POST JSONRPC. +Websocket connections are available at their own endpoint, typically `/websocket`, though this is configurable when starting the server. # Server Definition @@ -102,7 +102,7 @@ go func() { Note that unix sockets are supported as well (eg. `/path/to/socket` instead of `0.0.0.0:8008`) Now see all available endpoints by sending a GET request to `0.0.0.0:8008`. -Each route is available as a GET request, as a JSONRPCv2 POST request, and via JSONRPCv2 over websockets +Each route is available as a GET request, as a JSONRPCv2 POST request, and via JSONRPCv2 over websockets. # Examples diff --git a/client/http_client.go b/client/http_client.go index 57da5d6ec..0c8bb6363 100644 --- a/client/http_client.go +++ b/client/http_client.go @@ -3,7 +3,6 @@ package rpcclient import ( "bytes" "encoding/json" - "errors" "fmt" "io/ioutil" "net" @@ -12,11 +11,16 @@ import ( "reflect" "strings" - . "github.com/tendermint/go-common" - "github.com/tendermint/go-rpc/types" - "github.com/tendermint/go-wire" + // cmn "github.com/tendermint/go-common" + rpctypes "github.com/tendermint/go-rpc/types" + wire "github.com/tendermint/go-wire" ) +// HTTPClient is a common interface for ClientJSONRPC and ClientURI. +type HTTPClient interface { + Call(method string, params []interface{}, result interface{}) (interface{}, error) +} + // TODO: Deprecate support for IP:PORT or /path/to/socket func makeHTTPDialer(remoteAddr string) (string, func(string, string) (net.Conn, error)) { @@ -49,11 +53,6 @@ func makeHTTPClient(remoteAddr string) (string, *http.Client) { //------------------------------------------------------------------------------------ -type Client interface { -} - -//------------------------------------------------------------------------------------ - // JSON rpc takes params as a slice type ClientJSONRPC struct { address string @@ -68,11 +67,11 @@ func NewClientJSONRPC(remote string) *ClientJSONRPC { } } -func (c *ClientJSONRPC) Call(method string, params []interface{}, result interface{}) (interface{}, error) { +func (c *ClientJSONRPC) Call(method string, params map[string]interface{}, result interface{}) (interface{}, error) { return c.call(method, params, result) } -func (c *ClientJSONRPC) call(method string, params []interface{}, result interface{}) (interface{}, error) { +func (c *ClientJSONRPC) call(method string, params map[string]interface{}, result interface{}) (interface{}, error) { // Make request and get responseBytes request := rpctypes.RPCRequest{ JSONRPC: "2.0", @@ -80,7 +79,10 @@ func (c *ClientJSONRPC) call(method string, params []interface{}, result interfa Params: params, ID: "", } - requestBytes := wire.JSONBytes(request) + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } requestBuf := bytes.NewBuffer(requestBytes) // log.Info(Fmt("RPC request to %v (%v): %v", c.remote, method, string(requestBytes))) httpResponse, err := c.client.Post(c.address, "text/json", requestBuf) @@ -145,16 +147,16 @@ func unmarshalResponseBytes(responseBytes []byte, result interface{}) (interface response := &rpctypes.RPCResponse{} err = json.Unmarshal(responseBytes, response) if err != nil { - return nil, errors.New(Fmt("Error unmarshalling rpc response: %v", err)) + return nil, fmt.Errorf("Error unmarshalling rpc response: %v", err) } errorStr := response.Error if errorStr != "" { - return nil, errors.New(Fmt("Response error: %v", errorStr)) + return nil, fmt.Errorf("Response error: %v", errorStr) } // unmarshal the RawMessage into the result result = wire.ReadJSONPtr(result, *response.Result, &err) if err != nil { - return nil, errors.New(Fmt("Error unmarshalling rpc response result: %v", err)) + return nil, fmt.Errorf("Error unmarshalling rpc response result: %v", err) } return result, nil } diff --git a/client/ws_client.go b/client/ws_client.go index 4d975f8ec..e5135d0af 100644 --- a/client/ws_client.go +++ b/client/ws_client.go @@ -8,8 +8,8 @@ import ( "time" "github.com/gorilla/websocket" - . "github.com/tendermint/go-common" - "github.com/tendermint/go-rpc/types" + cmn "github.com/tendermint/go-common" + rpctypes "github.com/tendermint/go-rpc/types" ) const ( @@ -19,7 +19,7 @@ const ( ) type WSClient struct { - BaseService + cmn.BaseService Address string // IP:PORT or /path/to/socket Endpoint string // /websocket/url/endpoint Dialer func(string, string) (net.Conn, error) @@ -39,7 +39,7 @@ func NewWSClient(remoteAddr, endpoint string) *WSClient { ResultsCh: make(chan json.RawMessage, wsResultsChannelCapacity), ErrorsCh: make(chan error, wsErrorsChannelCapacity), } - wsClient.BaseService = *NewBaseService(log, "WSClient", wsClient) + wsClient.BaseService = *cmn.NewBaseService(log, "WSClient", wsClient) return wsClient } @@ -122,7 +122,7 @@ func (wsc *WSClient) Subscribe(eventid string) error { JSONRPC: "2.0", ID: "", Method: "subscribe", - Params: []interface{}{eventid}, + Params: map[string]interface{}{"event": eventid}, }) return err } @@ -133,7 +133,7 @@ func (wsc *WSClient) Unsubscribe(eventid string) error { JSONRPC: "2.0", ID: "", Method: "unsubscribe", - Params: []interface{}{eventid}, + Params: map[string]interface{}{"event": eventid}, }) return err } diff --git a/rpc_test.go b/rpc_test.go index 1fcda0e5e..2c45c4c13 100644 --- a/rpc_test.go +++ b/rpc_test.go @@ -5,10 +5,10 @@ import ( "testing" "time" - "github.com/tendermint/go-rpc/client" - "github.com/tendermint/go-rpc/server" - "github.com/tendermint/go-rpc/types" - "github.com/tendermint/go-wire" + rpcclient "github.com/tendermint/go-rpc/client" + rpcserver "github.com/tendermint/go-rpc/server" + rpctypes "github.com/tendermint/go-rpc/types" + wire "github.com/tendermint/go-wire" ) // Client and Server should work over tcp or unix sockets @@ -88,7 +88,9 @@ func testURI(t *testing.T, cl *rpcclient.ClientURI) { func testJSONRPC(t *testing.T, cl *rpcclient.ClientJSONRPC) { val := "acbd" - params := []interface{}{val} + params := map[string]interface{}{ + "arg": val, + } var result Result _, err := cl.Call("status", params, &result) if err != nil { @@ -102,7 +104,9 @@ func testJSONRPC(t *testing.T, cl *rpcclient.ClientJSONRPC) { func testWS(t *testing.T, cl *rpcclient.WSClient) { val := "acbd" - params := []interface{}{val} + params := map[string]interface{}{ + "arg": val, + } err := cl.WriteJSON(rpctypes.RPCRequest{ JSONRPC: "2.0", ID: "", diff --git a/server/handlers.go b/server/handlers.go index 7a4ec1a45..beb664d9f 100644 --- a/server/handlers.go +++ b/server/handlers.go @@ -4,7 +4,6 @@ import ( "bytes" "encoding/hex" "encoding/json" - "errors" "fmt" "io/ioutil" "net/http" @@ -14,10 +13,10 @@ import ( "time" "github.com/gorilla/websocket" - . "github.com/tendermint/go-common" - "github.com/tendermint/go-events" - . "github.com/tendermint/go-rpc/types" - "github.com/tendermint/go-wire" + cmn "github.com/tendermint/go-common" + events "github.com/tendermint/go-events" + types "github.com/tendermint/go-rpc/types" + wire "github.com/tendermint/go-wire" ) // Adds a route for each function in the funcMap, as well as general jsonrpc and websocket handlers for all functions. @@ -105,75 +104,99 @@ func makeJSONRPCHandler(funcMap map[string]*RPCFunc) http.HandlerFunc { return } - var request RPCRequest + var request types.RPCRequest err := json.Unmarshal(b, &request) if err != nil { - WriteRPCResponseHTTP(w, NewRPCResponse("", nil, fmt.Sprintf("Error unmarshalling request: %v", err.Error()))) + WriteRPCResponseHTTP(w, types.NewRPCResponse("", nil, fmt.Sprintf("Error unmarshalling request: %v", err.Error()))) return } if len(r.URL.Path) > 1 { - WriteRPCResponseHTTP(w, NewRPCResponse(request.ID, nil, fmt.Sprintf("Invalid JSONRPC endpoint %s", r.URL.Path))) + WriteRPCResponseHTTP(w, types.NewRPCResponse(request.ID, nil, fmt.Sprintf("Invalid JSONRPC endpoint %s", r.URL.Path))) return } rpcFunc := funcMap[request.Method] if rpcFunc == nil { - WriteRPCResponseHTTP(w, NewRPCResponse(request.ID, nil, "RPC method unknown: "+request.Method)) + WriteRPCResponseHTTP(w, types.NewRPCResponse(request.ID, nil, "RPC method unknown: "+request.Method)) return } if rpcFunc.ws { - WriteRPCResponseHTTP(w, NewRPCResponse(request.ID, nil, "RPC method is only for websockets: "+request.Method)) + WriteRPCResponseHTTP(w, types.NewRPCResponse(request.ID, nil, "RPC method is only for websockets: "+request.Method)) return } args, err := jsonParamsToArgs(rpcFunc, request.Params) if err != nil { - WriteRPCResponseHTTP(w, NewRPCResponse(request.ID, nil, fmt.Sprintf("Error converting json params to arguments: %v", err.Error()))) + WriteRPCResponseHTTP(w, types.NewRPCResponse(request.ID, nil, fmt.Sprintf("Error converting json params to arguments: %v", err.Error()))) return } returns := rpcFunc.f.Call(args) log.Info("HTTPJSONRPC", "method", request.Method, "args", args, "returns", returns) result, err := unreflectResult(returns) if err != nil { - WriteRPCResponseHTTP(w, NewRPCResponse(request.ID, result, fmt.Sprintf("Error unreflecting result: %v", err.Error()))) + WriteRPCResponseHTTP(w, types.NewRPCResponse(request.ID, result, fmt.Sprintf("Error unreflecting result: %v", err.Error()))) return } - WriteRPCResponseHTTP(w, NewRPCResponse(request.ID, result, "")) + WriteRPCResponseHTTP(w, types.NewRPCResponse(request.ID, result, "")) } } // Convert a list of interfaces to properly typed values -func jsonParamsToArgs(rpcFunc *RPCFunc, params []interface{}) ([]reflect.Value, error) { +func jsonParamsToArgs(rpcFunc *RPCFunc, params map[string]interface{}) ([]reflect.Value, error) { if len(rpcFunc.argNames) != len(params) { - return nil, errors.New(fmt.Sprintf("Expected %v parameters (%v), got %v (%v)", - len(rpcFunc.argNames), rpcFunc.argNames, len(params), params)) + return nil, fmt.Errorf("Expected %v parameters (%v), got %v (%v)", + len(rpcFunc.argNames), rpcFunc.argNames, len(params), params) } + values := make([]reflect.Value, len(params)) - for i, p := range params { + + for name, param := range params { + i := indexOf(name, rpcFunc.argNames) + if -1 == i { + return nil, fmt.Errorf("%s is not an argument (args: %v)", name, rpcFunc.argNames) + } ty := rpcFunc.args[i] - v, err := _jsonObjectToArg(ty, p) + v, err := _jsonObjectToArg(ty, param) if err != nil { return nil, err } values[i] = v } + return values, nil } +// indexOf returns index of a string in a slice of strings, -1 if not found. +func indexOf(value string, values []string) int { + for i, v := range values { + if v == value { + return i + } + } + return -1 +} + // Same as above, but with the first param the websocket connection -func jsonParamsToArgsWS(rpcFunc *RPCFunc, params []interface{}, wsCtx WSRPCContext) ([]reflect.Value, error) { +func jsonParamsToArgsWS(rpcFunc *RPCFunc, params map[string]interface{}, wsCtx types.WSRPCContext) ([]reflect.Value, error) { if len(rpcFunc.argNames) != len(params) { - return nil, errors.New(fmt.Sprintf("Expected %v parameters (%v), got %v (%v)", - len(rpcFunc.argNames)-1, rpcFunc.argNames[1:], len(params), params)) + return nil, fmt.Errorf("Expected %v parameters (%v), got %v (%v)", + len(rpcFunc.argNames)-1, rpcFunc.argNames[1:], len(params), params) } + values := make([]reflect.Value, len(params)+1) values[0] = reflect.ValueOf(wsCtx) - for i, p := range params { + + for name, param := range params { + i := indexOf(name, rpcFunc.argNames) + if -1 == i { + return nil, fmt.Errorf("%s is not an argument (args: %v)", name, rpcFunc.argNames) + } ty := rpcFunc.args[i+1] - v, err := _jsonObjectToArg(ty, p) + v, err := _jsonObjectToArg(ty, param) if err != nil { return nil, err } values[i+1] = v } + return values, nil } @@ -197,7 +220,7 @@ func makeHTTPHandler(rpcFunc *RPCFunc) func(http.ResponseWriter, *http.Request) // Exception for websocket endpoints if rpcFunc.ws { return func(w http.ResponseWriter, r *http.Request) { - WriteRPCResponseHTTP(w, NewRPCResponse("", nil, "This RPC method is only for websockets")) + WriteRPCResponseHTTP(w, types.NewRPCResponse("", nil, "This RPC method is only for websockets")) } } // All other endpoints @@ -205,17 +228,17 @@ func makeHTTPHandler(rpcFunc *RPCFunc) func(http.ResponseWriter, *http.Request) log.Debug("HTTP HANDLER", "req", r) args, err := httpParamsToArgs(rpcFunc, r) if err != nil { - WriteRPCResponseHTTP(w, NewRPCResponse("", nil, fmt.Sprintf("Error converting http params to args: %v", err.Error()))) + WriteRPCResponseHTTP(w, types.NewRPCResponse("", nil, fmt.Sprintf("Error converting http params to args: %v", err.Error()))) return } returns := rpcFunc.f.Call(args) log.Info("HTTPRestRPC", "method", r.URL.Path, "args", args, "returns", returns) result, err := unreflectResult(returns) if err != nil { - WriteRPCResponseHTTP(w, NewRPCResponse("", nil, fmt.Sprintf("Error unreflecting result: %v", err.Error()))) + WriteRPCResponseHTTP(w, types.NewRPCResponse("", nil, fmt.Sprintf("Error unreflecting result: %v", err.Error()))) return } - WriteRPCResponseHTTP(w, NewRPCResponse("", result, "")) + WriteRPCResponseHTTP(w, types.NewRPCResponse("", result, "")) } } @@ -313,11 +336,11 @@ const ( // contains listener id, underlying ws connection, // and the event switch for subscribing to events type wsConnection struct { - BaseService + cmn.BaseService remoteAddr string baseConn *websocket.Conn - writeChan chan RPCResponse + writeChan chan types.RPCResponse readTimeout *time.Timer pingTicker *time.Ticker @@ -330,11 +353,11 @@ func NewWSConnection(baseConn *websocket.Conn, funcMap map[string]*RPCFunc, evsw wsc := &wsConnection{ remoteAddr: baseConn.RemoteAddr().String(), baseConn: baseConn, - writeChan: make(chan RPCResponse, writeChanCapacity), // error when full. + writeChan: make(chan types.RPCResponse, writeChanCapacity), // error when full. funcMap: funcMap, evsw: evsw, } - wsc.BaseService = *NewBaseService(log, "wsConnection", wsc) + wsc.BaseService = *cmn.NewBaseService(log, "wsConnection", wsc) return wsc } @@ -399,7 +422,7 @@ func (wsc *wsConnection) GetEventSwitch() events.EventSwitch { // Implements WSRPCConnection // Blocking write to writeChan until service stops. // Goroutine-safe -func (wsc *wsConnection) WriteRPCResponse(resp RPCResponse) { +func (wsc *wsConnection) WriteRPCResponse(resp types.RPCResponse) { select { case <-wsc.Quit: return @@ -410,7 +433,7 @@ func (wsc *wsConnection) WriteRPCResponse(resp RPCResponse) { // Implements WSRPCConnection // Nonblocking write. // Goroutine-safe -func (wsc *wsConnection) TryWriteRPCResponse(resp RPCResponse) bool { +func (wsc *wsConnection) TryWriteRPCResponse(resp types.RPCResponse) bool { select { case <-wsc.Quit: return false @@ -444,11 +467,11 @@ func (wsc *wsConnection) readRoutine() { wsc.Stop() return } - var request RPCRequest + var request types.RPCRequest err = json.Unmarshal(in, &request) if err != nil { errStr := fmt.Sprintf("Error unmarshaling data: %s", err.Error()) - wsc.WriteRPCResponse(NewRPCResponse(request.ID, nil, errStr)) + wsc.WriteRPCResponse(types.NewRPCResponse(request.ID, nil, errStr)) continue } @@ -456,28 +479,28 @@ func (wsc *wsConnection) readRoutine() { rpcFunc := wsc.funcMap[request.Method] if rpcFunc == nil { - wsc.WriteRPCResponse(NewRPCResponse(request.ID, nil, "RPC method unknown: "+request.Method)) + wsc.WriteRPCResponse(types.NewRPCResponse(request.ID, nil, "RPC method unknown: "+request.Method)) continue } var args []reflect.Value if rpcFunc.ws { - wsCtx := WSRPCContext{Request: request, WSRPCConnection: wsc} + wsCtx := types.WSRPCContext{Request: request, WSRPCConnection: wsc} args, err = jsonParamsToArgsWS(rpcFunc, request.Params, wsCtx) } else { args, err = jsonParamsToArgs(rpcFunc, request.Params) } if err != nil { - wsc.WriteRPCResponse(NewRPCResponse(request.ID, nil, err.Error())) + wsc.WriteRPCResponse(types.NewRPCResponse(request.ID, nil, err.Error())) continue } returns := rpcFunc.f.Call(args) log.Info("WSJSONRPC", "method", request.Method, "args", args, "returns", returns) result, err := unreflectResult(returns) if err != nil { - wsc.WriteRPCResponse(NewRPCResponse(request.ID, nil, err.Error())) + wsc.WriteRPCResponse(types.NewRPCResponse(request.ID, nil, err.Error())) continue } else { - wsc.WriteRPCResponse(NewRPCResponse(request.ID, result, "")) + wsc.WriteRPCResponse(types.NewRPCResponse(request.ID, result, "")) continue } diff --git a/test/data.json b/test/data.json index eac2e0dfe..83283ec33 100644 --- a/test/data.json +++ b/test/data.json @@ -1,6 +1,9 @@ { - "jsonrpc":"2.0", - "id":"", - "method":"hello_world", - "params":["my_world", 5] + "jsonrpc": "2.0", + "id": "", + "method": "hello_world", + "params": { + "name": "my_world", + "num": 5 + } } diff --git a/test/test.sh b/test/integration_test.sh similarity index 65% rename from test/test.sh rename to test/integration_test.sh index f5e740241..739708068 100755 --- a/test/test.sh +++ b/test/integration_test.sh @@ -1,17 +1,13 @@ -#! /bin/bash - -cd $GOPATH/src/github.com/tendermint/go-rpc - -# get deps -go get -u -t ./... - -# go tests -go test --race github.com/tendermint/go-rpc/... +#!/usr/bin/env bash +set -e +# Get the directory of where this script is. +SOURCE="${BASH_SOURCE[0]}" +while [ -h "$SOURCE" ] ; do SOURCE="$(readlink "$SOURCE")"; done +DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )" -# integration tests -cd test -set -e +# Change into that dir because we expect that. +pushd "$DIR" go build -o server main.go ./server > /dev/null & @@ -19,8 +15,8 @@ PID=$! sleep 2 # simple request -R1=`curl -s 'http://localhost:8008/hello_world?name="my_world"&num=5'` -R2=`curl -s --data @data.json http://localhost:8008` +R1=$(curl -s 'http://localhost:8008/hello_world?name="my_world"&num=5') +R2=$(curl -s --data @data.json http://localhost:8008) if [[ "$R1" != "$R2" ]]; then echo "responses are not identical:" echo "R1: $R1" @@ -31,7 +27,7 @@ else fi # request with 0x-prefixed hex string arg -R1=`curl -s 'http://localhost:8008/hello_world?name=0x41424344&num=123'` +R1=$(curl -s 'http://localhost:8008/hello_world?name=0x41424344&num=123') R2='{"jsonrpc":"2.0","id":"","result":{"Result":"hi ABCD 123"},"error":""}' if [[ "$R1" != "$R2" ]]; then echo "responses are not identical:" @@ -43,7 +39,7 @@ else fi # request with unquoted string arg -R1=`curl -s 'http://localhost:8008/hello_world?name=abcd&num=123'` +R1=$(curl -s 'http://localhost:8008/hello_world?name=abcd&num=123') R2="{\"jsonrpc\":\"2.0\",\"id\":\"\",\"result\":null,\"error\":\"Error converting http params to args: invalid character 'a' looking for beginning of value\"}" if [[ "$R1" != "$R2" ]]; then echo "responses are not identical:" @@ -55,7 +51,7 @@ else fi # request with string type when expecting number arg -R1=`curl -s 'http://localhost:8008/hello_world?name="abcd"&num=0xabcd'` +R1=$(curl -s 'http://localhost:8008/hello_world?name="abcd"&num=0xabcd') R2="{\"jsonrpc\":\"2.0\",\"id\":\"\",\"result\":null,\"error\":\"Error converting http params to args: Got a hex string arg, but expected 'int'\"}" if [[ "$R1" != "$R2" ]]; then echo "responses are not identical:" @@ -67,3 +63,4 @@ else fi kill -9 $PID || exit 0 +popd diff --git a/test/main.go b/test/main.go index d14ab05f5..28de2be88 100644 --- a/test/main.go +++ b/test/main.go @@ -4,7 +4,7 @@ import ( "fmt" "net/http" - . "github.com/tendermint/go-common" + cmn "github.com/tendermint/go-common" rpcserver "github.com/tendermint/go-rpc/server" ) @@ -25,11 +25,11 @@ func main() { rpcserver.RegisterRPCFuncs(mux, routes) _, err := rpcserver.StartHTTPServer("0.0.0.0:8008", mux) if err != nil { - Exit(err.Error()) + cmn.Exit(err.Error()) } // Wait forever - TrapSignal(func() { + cmn.TrapSignal(func() { }) } diff --git a/types/types.go b/types/types.go index ee4a63cc8..cebd7564a 100644 --- a/types/types.go +++ b/types/types.go @@ -4,18 +4,18 @@ import ( "encoding/json" "strings" - "github.com/tendermint/go-events" - "github.com/tendermint/go-wire" + events "github.com/tendermint/go-events" + wire "github.com/tendermint/go-wire" ) type RPCRequest struct { - JSONRPC string `json:"jsonrpc"` - ID string `json:"id"` - Method string `json:"method"` - Params []interface{} `json:"params"` + JSONRPC string `json:"jsonrpc"` + ID string `json:"id"` + Method string `json:"method"` + Params map[string]interface{} `json:"params"` } -func NewRPCRequest(id string, method string, params []interface{}) RPCRequest { +func NewRPCRequest(id string, method string, params map[string]interface{}) RPCRequest { return RPCRequest{ JSONRPC: "2.0", ID: id, From 66867bf94984dced859e24075dd5b462f3f09b8b Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Tue, 7 Mar 2017 19:04:24 +0400 Subject: [PATCH 03/38] remove "rpc" prefix from package imports --- client/http_client.go | 9 ++++---- client/ws_client.go | 8 +++---- rpc_test.go | 49 ++++++++++++++++++++++--------------------- server/http_server.go | 14 ++++++------- 4 files changed, 39 insertions(+), 41 deletions(-) diff --git a/client/http_client.go b/client/http_client.go index 0c8bb6363..c11f8b3e3 100644 --- a/client/http_client.go +++ b/client/http_client.go @@ -11,8 +11,7 @@ import ( "reflect" "strings" - // cmn "github.com/tendermint/go-common" - rpctypes "github.com/tendermint/go-rpc/types" + types "github.com/tendermint/go-rpc/types" wire "github.com/tendermint/go-wire" ) @@ -28,7 +27,7 @@ func makeHTTPDialer(remoteAddr string) (string, func(string, string) (net.Conn, var protocol, address string if len(parts) != 2 { log.Warn("WARNING (go-rpc): Please use fully formed listening addresses, including the tcp:// or unix:// prefix") - protocol = rpctypes.SocketType(remoteAddr) + protocol = types.SocketType(remoteAddr) address = remoteAddr } else { protocol, address = parts[0], parts[1] @@ -73,7 +72,7 @@ func (c *ClientJSONRPC) Call(method string, params map[string]interface{}, resul func (c *ClientJSONRPC) call(method string, params map[string]interface{}, result interface{}) (interface{}, error) { // Make request and get responseBytes - request := rpctypes.RPCRequest{ + request := types.RPCRequest{ JSONRPC: "2.0", Method: method, Params: params, @@ -144,7 +143,7 @@ func unmarshalResponseBytes(responseBytes []byte, result interface{}) (interface // into the correct type // log.Notice("response", "response", string(responseBytes)) var err error - response := &rpctypes.RPCResponse{} + response := &types.RPCResponse{} err = json.Unmarshal(responseBytes, response) if err != nil { return nil, fmt.Errorf("Error unmarshalling rpc response: %v", err) diff --git a/client/ws_client.go b/client/ws_client.go index e5135d0af..d27e499dd 100644 --- a/client/ws_client.go +++ b/client/ws_client.go @@ -9,7 +9,7 @@ import ( "github.com/gorilla/websocket" cmn "github.com/tendermint/go-common" - rpctypes "github.com/tendermint/go-rpc/types" + types "github.com/tendermint/go-rpc/types" ) const ( @@ -96,7 +96,7 @@ func (wsc *WSClient) receiveEventsRoutine() { wsc.Stop() break } else { - var response rpctypes.RPCResponse + var response types.RPCResponse err := json.Unmarshal(data, &response) if err != nil { log.Info("WSClient failed to parse message", "error", err, "data", string(data)) @@ -118,7 +118,7 @@ func (wsc *WSClient) receiveEventsRoutine() { // subscribe to an event func (wsc *WSClient) Subscribe(eventid string) error { - err := wsc.WriteJSON(rpctypes.RPCRequest{ + err := wsc.WriteJSON(types.RPCRequest{ JSONRPC: "2.0", ID: "", Method: "subscribe", @@ -129,7 +129,7 @@ func (wsc *WSClient) Subscribe(eventid string) error { // unsubscribe from an event func (wsc *WSClient) Unsubscribe(eventid string) error { - err := wsc.WriteJSON(rpctypes.RPCRequest{ + err := wsc.WriteJSON(types.RPCRequest{ JSONRPC: "2.0", ID: "", Method: "unsubscribe", diff --git a/rpc_test.go b/rpc_test.go index 2c45c4c13..c77799199 100644 --- a/rpc_test.go +++ b/rpc_test.go @@ -5,14 +5,14 @@ import ( "testing" "time" - rpcclient "github.com/tendermint/go-rpc/client" - rpcserver "github.com/tendermint/go-rpc/server" - rpctypes "github.com/tendermint/go-rpc/types" + client "github.com/tendermint/go-rpc/client" + server "github.com/tendermint/go-rpc/server" + types "github.com/tendermint/go-rpc/types" wire "github.com/tendermint/go-wire" ) // Client and Server should work over tcp or unix sockets -var ( +const ( tcpAddr = "tcp://0.0.0.0:46657" unixAddr = "unix:///tmp/go-rpc.sock" // NOTE: must remove file for test to run again @@ -32,8 +32,8 @@ var _ = wire.RegisterInterface( ) // Define some routes -var Routes = map[string]*rpcserver.RPCFunc{ - "status": rpcserver.NewRPCFunc(StatusResult, "arg"), +var Routes = map[string]*server.RPCFunc{ + "status": server.NewRPCFunc(StatusResult, "arg"), } // an rpc function @@ -43,23 +43,24 @@ func StatusResult(v string) (Result, error) { // launch unix and tcp servers func init() { + mux := http.NewServeMux() - rpcserver.RegisterRPCFuncs(mux, Routes) - wm := rpcserver.NewWebsocketManager(Routes, nil) + server.RegisterRPCFuncs(mux, Routes) + wm := server.NewWebsocketManager(Routes, nil) mux.HandleFunc(websocketEndpoint, wm.WebsocketHandler) go func() { - _, err := rpcserver.StartHTTPServer(tcpAddr, mux) + _, err := server.StartHTTPServer(tcpAddr, mux) if err != nil { panic(err) } }() mux2 := http.NewServeMux() - rpcserver.RegisterRPCFuncs(mux2, Routes) - wm = rpcserver.NewWebsocketManager(Routes, nil) + server.RegisterRPCFuncs(mux2, Routes) + wm = server.NewWebsocketManager(Routes, nil) mux2.HandleFunc(websocketEndpoint, wm.WebsocketHandler) go func() { - _, err := rpcserver.StartHTTPServer(unixAddr, mux2) + _, err := server.StartHTTPServer(unixAddr, mux2) if err != nil { panic(err) } @@ -70,7 +71,7 @@ func init() { } -func testURI(t *testing.T, cl *rpcclient.ClientURI) { +func testURI(t *testing.T, cl *client.ClientURI) { val := "acbd" params := map[string]interface{}{ "arg": val, @@ -86,7 +87,7 @@ func testURI(t *testing.T, cl *rpcclient.ClientURI) { } } -func testJSONRPC(t *testing.T, cl *rpcclient.ClientJSONRPC) { +func testJSONRPC(t *testing.T, cl *client.ClientJSONRPC) { val := "acbd" params := map[string]interface{}{ "arg": val, @@ -102,12 +103,12 @@ func testJSONRPC(t *testing.T, cl *rpcclient.ClientJSONRPC) { } } -func testWS(t *testing.T, cl *rpcclient.WSClient) { +func testWS(t *testing.T, cl *client.WSClient) { val := "acbd" params := map[string]interface{}{ "arg": val, } - err := cl.WriteJSON(rpctypes.RPCRequest{ + err := cl.WriteJSON(types.RPCRequest{ JSONRPC: "2.0", ID: "", Method: "status", @@ -132,27 +133,27 @@ func testWS(t *testing.T, cl *rpcclient.WSClient) { //------------- func TestURI_TCP(t *testing.T) { - cl := rpcclient.NewClientURI(tcpAddr) + cl := client.NewClientURI(tcpAddr) testURI(t, cl) } func TestURI_UNIX(t *testing.T) { - cl := rpcclient.NewClientURI(unixAddr) + cl := client.NewClientURI(unixAddr) testURI(t, cl) } func TestJSONRPC_TCP(t *testing.T) { - cl := rpcclient.NewClientJSONRPC(tcpAddr) + cl := client.NewClientJSONRPC(tcpAddr) testJSONRPC(t, cl) } func TestJSONRPC_UNIX(t *testing.T) { - cl := rpcclient.NewClientJSONRPC(unixAddr) + cl := client.NewClientJSONRPC(unixAddr) testJSONRPC(t, cl) } func TestWS_TCP(t *testing.T) { - cl := rpcclient.NewWSClient(tcpAddr, websocketEndpoint) + cl := client.NewWSClient(tcpAddr, websocketEndpoint) _, err := cl.Start() if err != nil { t.Fatal(err) @@ -161,7 +162,7 @@ func TestWS_TCP(t *testing.T) { } func TestWS_UNIX(t *testing.T) { - cl := rpcclient.NewWSClient(unixAddr, websocketEndpoint) + cl := client.NewWSClient(unixAddr, websocketEndpoint) _, err := cl.Start() if err != nil { t.Fatal(err) @@ -170,7 +171,7 @@ func TestWS_UNIX(t *testing.T) { } func TestHexStringArg(t *testing.T) { - cl := rpcclient.NewClientURI(tcpAddr) + cl := client.NewClientURI(tcpAddr) // should NOT be handled as hex val := "0xabc" params := map[string]interface{}{ @@ -188,7 +189,7 @@ func TestHexStringArg(t *testing.T) { } func TestQuotedStringArg(t *testing.T) { - cl := rpcclient.NewClientURI(tcpAddr) + cl := client.NewClientURI(tcpAddr) // should NOT be unquoted val := "\"abc\"" params := map[string]interface{}{ diff --git a/server/http_server.go b/server/http_server.go index 26163cf12..24b9f18af 100644 --- a/server/http_server.go +++ b/server/http_server.go @@ -11,9 +11,7 @@ import ( "strings" "time" - . "github.com/tendermint/go-common" - . "github.com/tendermint/go-rpc/types" - //"github.com/tendermint/go-wire" + types "github.com/tendermint/go-rpc/types" ) func StartHTTPServer(listenAddr string, handler http.Handler) (listener net.Listener, err error) { @@ -24,14 +22,14 @@ func StartHTTPServer(listenAddr string, handler http.Handler) (listener net.List log.Warn("WARNING (go-rpc): Please use fully formed listening addresses, including the tcp:// or unix:// prefix") // we used to allow addrs without tcp/unix prefix by checking for a colon // TODO: Deprecate - proto = SocketType(listenAddr) + proto = types.SocketType(listenAddr) addr = listenAddr // return nil, fmt.Errorf("Invalid listener address %s", lisenAddr) } else { proto, addr = parts[0], parts[1] } - log.Notice(Fmt("Starting RPC HTTP server on %s socket %v", proto, addr)) + log.Notice(fmt.Sprintf("Starting RPC HTTP server on %s socket %v", proto, addr)) listener, err = net.Listen(proto, addr) if err != nil { return nil, fmt.Errorf("Failed to listen to %v: %v", listenAddr, err) @@ -47,7 +45,7 @@ func StartHTTPServer(listenAddr string, handler http.Handler) (listener net.List return listener, nil } -func WriteRPCResponseHTTP(w http.ResponseWriter, res RPCResponse) { +func WriteRPCResponseHTTP(w http.ResponseWriter, res types.RPCResponse) { // jsonBytes := wire.JSONBytesPretty(res) jsonBytes, err := json.Marshal(res) if err != nil { @@ -83,13 +81,13 @@ func RecoverAndLogHandler(handler http.Handler) http.Handler { if e := recover(); e != nil { // If RPCResponse - if res, ok := e.(RPCResponse); ok { + if res, ok := e.(types.RPCResponse); ok { WriteRPCResponseHTTP(rww, res) } else { // For the rest, log.Error("Panic in RPC HTTP handler", "error", e, "stack", string(debug.Stack())) rww.WriteHeader(http.StatusInternalServerError) - WriteRPCResponseHTTP(rww, NewRPCResponse("", nil, Fmt("Internal Server Error: %v", e))) + WriteRPCResponseHTTP(rww, types.NewRPCResponse("", nil, fmt.Sprintf("Internal Server Error: %v", e))) } } From c128957723b151972ec65a3d3bf4e168ead09f1b Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Tue, 7 Mar 2017 19:09:58 +0400 Subject: [PATCH 04/38] "must remove file for test to run again" - no way I am doing this by hands, too lazy :) --- rpc_test.go | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/rpc_test.go b/rpc_test.go index c77799199..990122517 100644 --- a/rpc_test.go +++ b/rpc_test.go @@ -2,6 +2,7 @@ package rpc import ( "net/http" + "os/exec" "testing" "time" @@ -13,8 +14,10 @@ import ( // Client and Server should work over tcp or unix sockets const ( - tcpAddr = "tcp://0.0.0.0:46657" - unixAddr = "unix:///tmp/go-rpc.sock" // NOTE: must remove file for test to run again + tcpAddr = "tcp://0.0.0.0:46657" + + unixSocket = "/tmp/go-rpc.sock" + unixAddr = "unix:///tmp/go-rpc.sock" websocketEndpoint = "/websocket/endpoint" ) @@ -43,6 +46,12 @@ func StatusResult(v string) (Result, error) { // launch unix and tcp servers func init() { + cmd := exec.Command("rm", "-f", unixSocket) + err := cmd.Start() + if err != nil { + panic(err) + } + err = cmd.Wait() mux := http.NewServeMux() server.RegisterRPCFuncs(mux, Routes) From 26ccb4c94a0d214f4e1bd635b07c830620f5c6d5 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Tue, 7 Mar 2017 19:16:36 +0400 Subject: [PATCH 05/38] remove private call methods Q: what was the reason to create them? --- client/http_client.go | 8 -------- 1 file changed, 8 deletions(-) diff --git a/client/http_client.go b/client/http_client.go index c11f8b3e3..1585fd3b5 100644 --- a/client/http_client.go +++ b/client/http_client.go @@ -67,10 +67,6 @@ func NewClientJSONRPC(remote string) *ClientJSONRPC { } func (c *ClientJSONRPC) Call(method string, params map[string]interface{}, result interface{}) (interface{}, error) { - return c.call(method, params, result) -} - -func (c *ClientJSONRPC) call(method string, params map[string]interface{}, result interface{}) (interface{}, error) { // Make request and get responseBytes request := types.RPCRequest{ JSONRPC: "2.0", @@ -114,10 +110,6 @@ func NewClientURI(remote string) *ClientURI { } func (c *ClientURI) Call(method string, params map[string]interface{}, result interface{}) (interface{}, error) { - return c.call(method, params, result) -} - -func (c *ClientURI) call(method string, params map[string]interface{}, result interface{}) (interface{}, error) { values, err := argsToURLValues(params) if err != nil { return nil, err From d43e3db9789ba0baac1fbcbd3f9945c32ef4c9e0 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Tue, 7 Mar 2017 19:20:19 +0400 Subject: [PATCH 06/38] fix circleci --- Makefile | 2 +- circle.yml | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index e17fa06ac..759c5cc09 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ PACKAGES=$(shell go list ./...) -all: test +all: get_deps test test: @echo "--> Running go test --race" diff --git a/circle.yml b/circle.yml index 99af678c6..0308a4e79 100644 --- a/circle.yml +++ b/circle.yml @@ -11,12 +11,10 @@ checkout: - rm -rf $REPO - mkdir -p $HOME/.go_workspace/src/github.com/$CIRCLE_PROJECT_USERNAME - mv $HOME/$CIRCLE_PROJECT_REPONAME $REPO - # - git submodule sync - # - git submodule update --init # use submodules dependencies: override: - - "cd $REPO" + - "cd $REPO && make get_deps" test: override: From 22ba8bdef81b246965469374fe0e3521124d0356 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Wed, 8 Mar 2017 10:26:13 +0400 Subject: [PATCH 07/38] fix Call method signature in HTTPClient interface --- client/http_client.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/http_client.go b/client/http_client.go index 1585fd3b5..3eb35aa3c 100644 --- a/client/http_client.go +++ b/client/http_client.go @@ -17,7 +17,7 @@ import ( // HTTPClient is a common interface for ClientJSONRPC and ClientURI. type HTTPClient interface { - Call(method string, params []interface{}, result interface{}) (interface{}, error) + Call(method string, params map[string]interface{}, result interface{}) (interface{}, error) } // TODO: Deprecate support for IP:PORT or /path/to/socket From 51d760f29f4e54283498e2dec433d5a36e0e0814 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Wed, 8 Mar 2017 16:23:38 +0400 Subject: [PATCH 08/38] use local import for testing --- Makefile | 2 +- test/main.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 759c5cc09..a2e3bea7f 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -PACKAGES=$(shell go list ./...) +PACKAGES=$(shell go list ./... | grep -v "test") all: get_deps test diff --git a/test/main.go b/test/main.go index 28de2be88..670e3deae 100644 --- a/test/main.go +++ b/test/main.go @@ -4,8 +4,8 @@ import ( "fmt" "net/http" + rpcserver "../server" cmn "github.com/tendermint/go-common" - rpcserver "github.com/tendermint/go-rpc/server" ) var routes = map[string]*rpcserver.RPCFunc{ From 6d66cc68ed17c50985c4bae24158d9dc7eb4d58e Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Wed, 8 Mar 2017 16:24:04 +0400 Subject: [PATCH 09/38] make sure we are using correct server also remove it afterwards --- test/integration_test.sh | 32 ++++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/test/integration_test.sh b/test/integration_test.sh index 739708068..5c85704be 100755 --- a/test/integration_test.sh +++ b/test/integration_test.sh @@ -9,12 +9,19 @@ DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )" # Change into that dir because we expect that. pushd "$DIR" -go build -o server main.go -./server > /dev/null & +echo "==> Building the server" +go build -o rpcserver main.go + +echo "==> (Re)starting the server" +PID=$(pgrep rpcserver || echo "") +if [[ $PID != "" ]]; then + kill -9 "$PID" +fi +./rpcserver & PID=$! sleep 2 -# simple request +echo "==> simple request" R1=$(curl -s 'http://localhost:8008/hello_world?name="my_world"&num=5') R2=$(curl -s --data @data.json http://localhost:8008) if [[ "$R1" != "$R2" ]]; then @@ -23,10 +30,10 @@ if [[ "$R1" != "$R2" ]]; then echo "R2: $R2" exit 1 else - echo "Success" + echo "OK" fi -# request with 0x-prefixed hex string arg +echo "==> request with 0x-prefixed hex string arg" R1=$(curl -s 'http://localhost:8008/hello_world?name=0x41424344&num=123') R2='{"jsonrpc":"2.0","id":"","result":{"Result":"hi ABCD 123"},"error":""}' if [[ "$R1" != "$R2" ]]; then @@ -35,7 +42,7 @@ if [[ "$R1" != "$R2" ]]; then echo "R2: $R2" exit 1 else - echo "Success" + echo "OK" fi # request with unquoted string arg @@ -47,10 +54,10 @@ if [[ "$R1" != "$R2" ]]; then echo "R2: $R2" exit 1 else - echo "Success" + echo "OK" fi -# request with string type when expecting number arg +echo "==> request with string type when expecting number arg" R1=$(curl -s 'http://localhost:8008/hello_world?name="abcd"&num=0xabcd') R2="{\"jsonrpc\":\"2.0\",\"id\":\"\",\"result\":null,\"error\":\"Error converting http params to args: Got a hex string arg, but expected 'int'\"}" if [[ "$R1" != "$R2" ]]; then @@ -59,8 +66,13 @@ if [[ "$R1" != "$R2" ]]; then echo "R2: $R2" exit 1 else - echo "Success" + echo "OK" fi -kill -9 $PID || exit 0 +echo "==> Stopping the server" +kill -9 $PID + +rm -f rpcserver + popd +exit 0 From 2dc6ab3896e876d7ec2371b3faa6112971e9e849 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Wed, 8 Mar 2017 17:16:01 +0400 Subject: [PATCH 10/38] use golang default if an arg is missing (Refs #7) --- server/handlers.go | 54 +++++++++++++++++++++++----------------- test/integration_test.sh | 19 +++++++++++++- 2 files changed, 49 insertions(+), 24 deletions(-) diff --git a/server/handlers.go b/server/handlers.go index beb664d9f..7085b81e2 100644 --- a/server/handlers.go +++ b/server/handlers.go @@ -141,20 +141,20 @@ func makeJSONRPCHandler(funcMap map[string]*RPCFunc) http.HandlerFunc { // Convert a list of interfaces to properly typed values func jsonParamsToArgs(rpcFunc *RPCFunc, params map[string]interface{}) ([]reflect.Value, error) { - if len(rpcFunc.argNames) != len(params) { - return nil, fmt.Errorf("Expected %v parameters (%v), got %v (%v)", - len(rpcFunc.argNames), rpcFunc.argNames, len(params), params) - } + values := make([]reflect.Value, len(rpcFunc.args)) - values := make([]reflect.Value, len(params)) + // fill each value with default + for i, argType := range rpcFunc.args { + values[i] = reflect.Zero(argType) + } for name, param := range params { i := indexOf(name, rpcFunc.argNames) if -1 == i { return nil, fmt.Errorf("%s is not an argument (args: %v)", name, rpcFunc.argNames) } - ty := rpcFunc.args[i] - v, err := _jsonObjectToArg(ty, param) + argType := rpcFunc.args[i] + v, err := _jsonObjectToArg(argType, param) if err != nil { return nil, err } @@ -176,21 +176,21 @@ func indexOf(value string, values []string) int { // Same as above, but with the first param the websocket connection func jsonParamsToArgsWS(rpcFunc *RPCFunc, params map[string]interface{}, wsCtx types.WSRPCContext) ([]reflect.Value, error) { - if len(rpcFunc.argNames) != len(params) { - return nil, fmt.Errorf("Expected %v parameters (%v), got %v (%v)", - len(rpcFunc.argNames)-1, rpcFunc.argNames[1:], len(params), params) - } - - values := make([]reflect.Value, len(params)+1) + values := make([]reflect.Value, len(rpcFunc.args)) values[0] = reflect.ValueOf(wsCtx) + // fill each value with default + for i, argType := range rpcFunc.args { + values[i+1] = reflect.Zero(argType) + } + for name, param := range params { i := indexOf(name, rpcFunc.argNames) if -1 == i { return nil, fmt.Errorf("%s is not an argument (args: %v)", name, rpcFunc.argNames) } - ty := rpcFunc.args[i+1] - v, err := _jsonObjectToArg(ty, param) + argType := rpcFunc.args[i+1] + v, err := _jsonObjectToArg(argType, param) if err != nil { return nil, err } @@ -245,16 +245,23 @@ func makeHTTPHandler(rpcFunc *RPCFunc) func(http.ResponseWriter, *http.Request) // Covert an http query to a list of properly typed values. // To be properly decoded the arg must be a concrete type from tendermint (if its an interface). func httpParamsToArgs(rpcFunc *RPCFunc, r *http.Request) ([]reflect.Value, error) { - argTypes := rpcFunc.args - argNames := rpcFunc.argNames + values := make([]reflect.Value, len(rpcFunc.args)) - values := make([]reflect.Value, len(argNames)) - for i, name := range argNames { - ty := argTypes[i] + // fill each value with default + for i, argType := range rpcFunc.args { + values[i] = reflect.Zero(argType) + } + + for i, name := range rpcFunc.argNames { + argType := rpcFunc.args[i] arg := GetParam(r, name) - // log.Notice("param to arg", "ty", ty, "name", name, "arg", arg) + // log.Notice("param to arg", "argType", argType, "name", name, "arg", arg) + + if "" == arg { + continue + } - v, err, ok := nonJsonToArg(ty, arg) + v, err, ok := nonJsonToArg(argType, arg) if err != nil { return nil, err } @@ -264,11 +271,12 @@ func httpParamsToArgs(rpcFunc *RPCFunc, r *http.Request) ([]reflect.Value, error } // Pass values to go-wire - values[i], err = _jsonStringToArg(ty, arg) + values[i], err = _jsonStringToArg(argType, arg) if err != nil { return nil, err } } + return values, nil } diff --git a/test/integration_test.sh b/test/integration_test.sh index 5c85704be..ea246c896 100755 --- a/test/integration_test.sh +++ b/test/integration_test.sh @@ -28,6 +28,7 @@ if [[ "$R1" != "$R2" ]]; then echo "responses are not identical:" echo "R1: $R1" echo "R2: $R2" + echo "FAIL" exit 1 else echo "OK" @@ -40,18 +41,33 @@ if [[ "$R1" != "$R2" ]]; then echo "responses are not identical:" echo "R1: $R1" echo "R2: $R2" + echo "FAIL" exit 1 else echo "OK" fi -# request with unquoted string arg +echo "==> request with missing params" +R1=$(curl -s 'http://localhost:8008/hello_world') +R2='{"jsonrpc":"2.0","id":"","result":{"Result":"hi 0"},"error":""}' +if [[ "$R1" != "$R2" ]]; then + echo "responses are not identical:" + echo "R1: $R1" + echo "R2: $R2" + echo "FAIL" + exit 1 +else + echo "OK" +fi + +echo "==> request with unquoted string arg" R1=$(curl -s 'http://localhost:8008/hello_world?name=abcd&num=123') R2="{\"jsonrpc\":\"2.0\",\"id\":\"\",\"result\":null,\"error\":\"Error converting http params to args: invalid character 'a' looking for beginning of value\"}" if [[ "$R1" != "$R2" ]]; then echo "responses are not identical:" echo "R1: $R1" echo "R2: $R2" + echo "FAIL" exit 1 else echo "OK" @@ -64,6 +80,7 @@ if [[ "$R1" != "$R2" ]]; then echo "responses are not identical:" echo "R1: $R1" echo "R2: $R2" + echo "FAIL" exit 1 else echo "OK" From d033cd54b8c48a5621bbe8fa67833612643fb7d4 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Wed, 8 Mar 2017 17:17:00 +0400 Subject: [PATCH 11/38] add editorconfig --- .editorconfig | 15 +++++++++++++++ test/integration_test.sh | 12 ++++++------ 2 files changed, 21 insertions(+), 6 deletions(-) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..d587999e1 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +# top-most EditorConfig file +root = true + +# Unix-style newlines with a newline ending every file +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[Makefile] +indent_style = tab + +[*.sh] +indent_style = tab diff --git a/test/integration_test.sh b/test/integration_test.sh index ea246c896..7c23be7d3 100755 --- a/test/integration_test.sh +++ b/test/integration_test.sh @@ -15,7 +15,7 @@ go build -o rpcserver main.go echo "==> (Re)starting the server" PID=$(pgrep rpcserver || echo "") if [[ $PID != "" ]]; then - kill -9 "$PID" + kill -9 "$PID" fi ./rpcserver & PID=$! @@ -51,13 +51,13 @@ echo "==> request with missing params" R1=$(curl -s 'http://localhost:8008/hello_world') R2='{"jsonrpc":"2.0","id":"","result":{"Result":"hi 0"},"error":""}' if [[ "$R1" != "$R2" ]]; then - echo "responses are not identical:" - echo "R1: $R1" - echo "R2: $R2" + echo "responses are not identical:" + echo "R1: $R1" + echo "R2: $R2" echo "FAIL" - exit 1 + exit 1 else - echo "OK" + echo "OK" fi echo "==> request with unquoted string arg" From 1842e03315833fd61c070ba7ba13fd5bbf51e42b Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Wed, 8 Mar 2017 17:33:46 +0400 Subject: [PATCH 12/38] revert using local import this breaks the client's code (e.g. tendermint) --- test/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/main.go b/test/main.go index 670e3deae..28de2be88 100644 --- a/test/main.go +++ b/test/main.go @@ -4,8 +4,8 @@ import ( "fmt" "net/http" - rpcserver "../server" cmn "github.com/tendermint/go-common" + rpcserver "github.com/tendermint/go-rpc/server" ) var routes = map[string]*rpcserver.RPCFunc{ From fed84f875cfc59a22dfe98e68b93e1fb5151fef9 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Wed, 8 Mar 2017 17:55:08 +0400 Subject: [PATCH 13/38] fix jsonParamsToArgsWS index error Error from tendermint: ``` panic: runtime error: index out of range goroutine 82 [running]: github.com/tendermint/tendermint/vendor/github.com/tendermint/go-rpc/server.jsonParamsToArgsWS(0xc4200960e0, 0xc42024d4a0, 0xc420215380, 0x3, 0x0, 0x0, 0xc420215383, 0x9, 0xc42024d4a0, 0xf1ecc0, ...) /home/vagrant/go/src/github.com/tendermint/tendermint/vendor/github.com/tendermint/go-rpc/server/handlers.go:184 +0x654 github.com/tendermint/tendermint/vendor/github.com/tendermint/go-rpc/server.(*wsConnection).readRoutine(0xc4201fd0e0) /home/vagrant/go/src/github.com/tendermint/tendermint/vendor/github.com/tendermint/go-rpc/server/handlers.go:496 +0x3a9 created by github.com/tendermint/tendermint/vendor/github.com/tendermint/go-rpc/server.(*wsConnection).OnStart /home/vagrant/go/src/github.com/tendermint/tendermint/vendor/github.com/tendermint/go-rpc/server/handlers.go:377 +0x45 ``` --- server/handlers.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/handlers.go b/server/handlers.go index 7085b81e2..2a6102d1e 100644 --- a/server/handlers.go +++ b/server/handlers.go @@ -176,7 +176,7 @@ func indexOf(value string, values []string) int { // Same as above, but with the first param the websocket connection func jsonParamsToArgsWS(rpcFunc *RPCFunc, params map[string]interface{}, wsCtx types.WSRPCContext) ([]reflect.Value, error) { - values := make([]reflect.Value, len(rpcFunc.args)) + values := make([]reflect.Value, len(rpcFunc.args)+1) values[0] = reflect.ValueOf(wsCtx) // fill each value with default @@ -189,7 +189,7 @@ func jsonParamsToArgsWS(rpcFunc *RPCFunc, params map[string]interface{}, wsCtx t if -1 == i { return nil, fmt.Errorf("%s is not an argument (args: %v)", name, rpcFunc.argNames) } - argType := rpcFunc.args[i+1] + argType := rpcFunc.args[i] v, err := _jsonObjectToArg(argType, param) if err != nil { return nil, err From 1ddb60b6e73b2bb5c2d1845eaa763e0a8ad57855 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Thu, 9 Mar 2017 12:23:21 +0400 Subject: [PATCH 14/38] refactor jsonParamsToArgs Suggested in https://github.com/tendermint/go-rpc/pull/9#discussion_r105098390 --- server/handlers.go | 68 ++++++++++++---------------------------------- 1 file changed, 18 insertions(+), 50 deletions(-) diff --git a/server/handlers.go b/server/handlers.go index 2a6102d1e..6e746a26b 100644 --- a/server/handlers.go +++ b/server/handlers.go @@ -143,61 +143,31 @@ func makeJSONRPCHandler(funcMap map[string]*RPCFunc) http.HandlerFunc { func jsonParamsToArgs(rpcFunc *RPCFunc, params map[string]interface{}) ([]reflect.Value, error) { values := make([]reflect.Value, len(rpcFunc.args)) - // fill each value with default - for i, argType := range rpcFunc.args { - values[i] = reflect.Zero(argType) - } - - for name, param := range params { - i := indexOf(name, rpcFunc.argNames) - if -1 == i { - return nil, fmt.Errorf("%s is not an argument (args: %v)", name, rpcFunc.argNames) - } + for i, argName := range rpcFunc.argNames { argType := rpcFunc.args[i] - v, err := _jsonObjectToArg(argType, param) - if err != nil { - return nil, err + + // decode param if provided + if param, ok := params[argName]; ok && "" != param { + v, err := _jsonObjectToArg(argType, param) + if err != nil { + return nil, err + } + values[i] = v + } else { // use default for that type + values[i] = reflect.Zero(argType) } - values[i] = v } return values, nil } -// indexOf returns index of a string in a slice of strings, -1 if not found. -func indexOf(value string, values []string) int { - for i, v := range values { - if v == value { - return i - } - } - return -1 -} - // Same as above, but with the first param the websocket connection func jsonParamsToArgsWS(rpcFunc *RPCFunc, params map[string]interface{}, wsCtx types.WSRPCContext) ([]reflect.Value, error) { - values := make([]reflect.Value, len(rpcFunc.args)+1) - values[0] = reflect.ValueOf(wsCtx) - - // fill each value with default - for i, argType := range rpcFunc.args { - values[i+1] = reflect.Zero(argType) - } - - for name, param := range params { - i := indexOf(name, rpcFunc.argNames) - if -1 == i { - return nil, fmt.Errorf("%s is not an argument (args: %v)", name, rpcFunc.argNames) - } - argType := rpcFunc.args[i] - v, err := _jsonObjectToArg(argType, param) - if err != nil { - return nil, err - } - values[i+1] = v + values, err := jsonParamsToArgs(rpcFunc, params) + if err != nil { + return nil, err } - - return values, nil + return append([]reflect.Value{reflect.ValueOf(wsCtx)}, values...), nil } func _jsonObjectToArg(ty reflect.Type, object interface{}) (reflect.Value, error) { @@ -247,13 +217,11 @@ func makeHTTPHandler(rpcFunc *RPCFunc) func(http.ResponseWriter, *http.Request) func httpParamsToArgs(rpcFunc *RPCFunc, r *http.Request) ([]reflect.Value, error) { values := make([]reflect.Value, len(rpcFunc.args)) - // fill each value with default - for i, argType := range rpcFunc.args { - values[i] = reflect.Zero(argType) - } - for i, name := range rpcFunc.argNames { argType := rpcFunc.args[i] + + values[i] = reflect.Zero(argType) // set default for that type + arg := GetParam(r, name) // log.Notice("param to arg", "argType", argType, "name", name, "arg", arg) From cf11e6ba65423f81b9db3bc2550303d1a325e8ae Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Thu, 9 Mar 2017 12:43:24 +0400 Subject: [PATCH 15/38] add CHANGELOG --- README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/README.md b/README.md index 4da0e5634..149ecdf51 100644 --- a/README.md +++ b/README.md @@ -109,3 +109,18 @@ Each route is available as a GET request, as a JSONRPCv2 POST request, and via J * [Tendermint](https://github.com/tendermint/tendermint/blob/master/rpc/core/routes.go) * [Network Monitor](https://github.com/tendermint/netmon/blob/master/handlers/routes.go) + +## CHANGELOG + +### 0.7.0 + +BREAKING CHANGES: + +- removed `Client` empty interface +- `ClientJSONRPC#Call` `params` argument became a map + +IMPROVEMENTS: + +- added `HTTPClient` interface, which can be used for both `ClientURI` +and `ClientJSONRPC` +- all params are now optional (Golang's default will be used if some param is missing) From 05e1a22d5b6e91c9c00b0824f4f95e0efd0a34da Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Thu, 9 Mar 2017 13:46:48 +0400 Subject: [PATCH 16/38] encode params before sending in JSONRPC --- client/http_client.go | 9 +++++++-- rpc_test.go | 41 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/client/http_client.go b/client/http_client.go index 3eb35aa3c..960869b75 100644 --- a/client/http_client.go +++ b/client/http_client.go @@ -67,11 +67,16 @@ func NewClientJSONRPC(remote string) *ClientJSONRPC { } func (c *ClientJSONRPC) Call(method string, params map[string]interface{}, result interface{}) (interface{}, error) { - // Make request and get responseBytes + // we need this step because we attempt to decode values using `go-wire` + // (handlers.go:176) on the server side + encodedParams := make(map[string]interface{}) + for k, v := range params { + encodedParams[k] = json.RawMessage(wire.JSONBytes(v)) + } request := types.RPCRequest{ JSONRPC: "2.0", Method: method, - Params: params, + Params: encodedParams, ID: "", } requestBytes, err := json.Marshal(request) diff --git a/rpc_test.go b/rpc_test.go index 990122517..a719aee55 100644 --- a/rpc_test.go +++ b/rpc_test.go @@ -1,6 +1,9 @@ package rpc import ( + "bytes" + crand "crypto/rand" + "math/rand" "net/http" "os/exec" "testing" @@ -29,14 +32,20 @@ type ResultStatus struct { Value string } +type ResultBytes struct { + Value []byte +} + var _ = wire.RegisterInterface( struct{ Result }{}, wire.ConcreteType{&ResultStatus{}, 0x1}, + wire.ConcreteType{&ResultBytes{}, 0x2}, ) // Define some routes var Routes = map[string]*server.RPCFunc{ "status": server.NewRPCFunc(StatusResult, "arg"), + "bytes": server.NewRPCFunc(BytesResult, "arg"), } // an rpc function @@ -44,6 +53,10 @@ func StatusResult(v string) (Result, error) { return &ResultStatus{v}, nil } +func BytesResult(v []byte) (Result, error) { + return &ResultBytes{v}, nil +} + // launch unix and tcp servers func init() { cmd := exec.Command("rm", "-f", unixSocket) @@ -214,3 +227,31 @@ func TestQuotedStringArg(t *testing.T) { t.Fatalf("Got: %v .... Expected: %v \n", got, val) } } + +func randBytes(t *testing.T) []byte { + n := rand.Intn(10) + 2 + buf := make([]byte, n) + _, err := crand.Read(buf) + if err != nil { + t.Fatal(err) + } + return bytes.Replace(buf, []byte("="), []byte{100}, -1) +} + +func TestByteSliceViaJSONRPC(t *testing.T) { + cl := client.NewClientJSONRPC(unixAddr) + + val := randBytes(t) + params := map[string]interface{}{ + "arg": val, + } + var result Result + _, err := cl.Call("bytes", params, &result) + if err != nil { + t.Fatal(err) + } + got := result.(*ResultBytes).Value + if bytes.Compare(got, val) != 0 { + t.Fatalf("Got: %v .... Expected: %v \n", got, val) + } +} From 720b74d89edd0b9e7406e7b4557352448b4c0c16 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Thu, 9 Mar 2017 17:44:00 +0400 Subject: [PATCH 17/38] read from ErrorsCh also --- rpc_test.go | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/rpc_test.go b/rpc_test.go index a719aee55..074c212a8 100644 --- a/rpc_test.go +++ b/rpc_test.go @@ -140,16 +140,20 @@ func testWS(t *testing.T, cl *client.WSClient) { t.Fatal(err) } - msg := <-cl.ResultsCh - result := new(Result) - wire.ReadJSONPtr(result, msg, &err) - if err != nil { + select { + case msg := <-cl.ResultsCh: + result := new(Result) + wire.ReadJSONPtr(result, msg, &err) + if err != nil { + t.Fatal(err) + } + got := (*result).(*ResultStatus).Value + if got != val { + t.Fatalf("Got: %v .... Expected: %v \n", got, val) + } + case err := <-cl.ErrorsCh: t.Fatal(err) } - got := (*result).(*ResultStatus).Value - if got != val { - t.Fatalf("Got: %v .... Expected: %v \n", got, val) - } } //------------- From ff90224ba8166ebb2df545af44f3f832ac3452c5 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Thu, 9 Mar 2017 18:30:55 +0400 Subject: [PATCH 18/38] fix "Expected map but got type string" error Error from tendermint: ``` panic: Expected map but got type string [recovered] panic: Expected map but got type string goroutine 82 [running]: testing.tRunner.func1(0xc420464000) /usr/local/go/src/testing/testing.go:622 +0x29d panic(0xa1fda0, 0xc4201eecd0) /usr/local/go/src/runtime/panic.go:489 +0x2cf github.com/tendermint/tendermint/rpc/test.waitForEvent(0xc420464000, 0xc420064000, 0xae6fae, 0x8, 0xae6f01, 0xc2e998, 0xc2e9a0) /home/vagrant/go/src/github.com/tendermint/tendermint/rpc/test/helpers.go:179 +0x53a github.com/tendermint/tendermint/rpc/test.TestWSNewBlock(0xc420464000) /home/vagrant/go/src/github.com/tendermint/tendermint/rpc/test/client_test.go:190 +0x12e testing.tRunner(0xc420464000, 0xc2e9a8) /usr/local/go/src/testing/testing.go:657 +0x96 created by testing.(*T).Run /usr/local/go/src/testing/testing.go:697 +0x2ca ``` --- rpc_test.go | 47 ++++++++++++++++++++++++++++++++++++++++++++-- server/handlers.go | 17 +++++++++++------ 2 files changed, 56 insertions(+), 8 deletions(-) diff --git a/rpc_test.go b/rpc_test.go index 074c212a8..334909f1f 100644 --- a/rpc_test.go +++ b/rpc_test.go @@ -44,8 +44,9 @@ var _ = wire.RegisterInterface( // Define some routes var Routes = map[string]*server.RPCFunc{ - "status": server.NewRPCFunc(StatusResult, "arg"), - "bytes": server.NewRPCFunc(BytesResult, "arg"), + "status": server.NewRPCFunc(StatusResult, "arg"), + "status_ws": server.NewWSRPCFunc(StatusWSResult, "arg"), + "bytes": server.NewRPCFunc(BytesResult, "arg"), } // an rpc function @@ -53,6 +54,10 @@ func StatusResult(v string) (Result, error) { return &ResultStatus{v}, nil } +func StatusWSResult(wsCtx types.WSRPCContext, v string) (Result, error) { + return &ResultStatus{v}, nil +} + func BytesResult(v []byte) (Result, error) { return &ResultBytes{v}, nil } @@ -259,3 +264,41 @@ func TestByteSliceViaJSONRPC(t *testing.T) { t.Fatalf("Got: %v .... Expected: %v \n", got, val) } } + +func TestWSNewWSRPCFunc(t *testing.T) { + cl := client.NewWSClient(unixAddr, websocketEndpoint) + _, err := cl.Start() + if err != nil { + t.Fatal(err) + } + defer cl.Stop() + + val := "acbd" + params := map[string]interface{}{ + "arg": val, + } + err = cl.WriteJSON(types.RPCRequest{ + JSONRPC: "2.0", + ID: "", + Method: "status_ws", + Params: params, + }) + if err != nil { + t.Fatal(err) + } + + select { + case msg := <-cl.ResultsCh: + result := new(Result) + wire.ReadJSONPtr(result, msg, &err) + if err != nil { + t.Fatal(err) + } + got := (*result).(*ResultStatus).Value + if got != val { + t.Fatalf("Got: %v .... Expected: %v \n", got, val) + } + case err := <-cl.ErrorsCh: + t.Fatal(err) + } +} diff --git a/server/handlers.go b/server/handlers.go index 6e746a26b..e590a45fd 100644 --- a/server/handlers.go +++ b/server/handlers.go @@ -123,7 +123,7 @@ func makeJSONRPCHandler(funcMap map[string]*RPCFunc) http.HandlerFunc { WriteRPCResponseHTTP(w, types.NewRPCResponse(request.ID, nil, "RPC method is only for websockets: "+request.Method)) return } - args, err := jsonParamsToArgs(rpcFunc, request.Params) + args, err := jsonParamsToArgs(rpcFunc, request.Params, 0) if err != nil { WriteRPCResponseHTTP(w, types.NewRPCResponse(request.ID, nil, fmt.Sprintf("Error converting json params to arguments: %v", err.Error()))) return @@ -140,11 +140,16 @@ func makeJSONRPCHandler(funcMap map[string]*RPCFunc) http.HandlerFunc { } // Convert a list of interfaces to properly typed values -func jsonParamsToArgs(rpcFunc *RPCFunc, params map[string]interface{}) ([]reflect.Value, error) { - values := make([]reflect.Value, len(rpcFunc.args)) +// +// argsOffset is used in jsonParamsToArgsWS, where len(rpcFunc.args) != len(rpcFunc.argNames). +// Example: +// rpcFunc.args = [rpctypes.WSRPCContext string] +// rpcFunc.argNames = ["arg"] +func jsonParamsToArgs(rpcFunc *RPCFunc, params map[string]interface{}, argsOffset int) ([]reflect.Value, error) { + values := make([]reflect.Value, len(rpcFunc.argNames)) for i, argName := range rpcFunc.argNames { - argType := rpcFunc.args[i] + argType := rpcFunc.args[i+argsOffset] // decode param if provided if param, ok := params[argName]; ok && "" != param { @@ -163,7 +168,7 @@ func jsonParamsToArgs(rpcFunc *RPCFunc, params map[string]interface{}) ([]reflec // Same as above, but with the first param the websocket connection func jsonParamsToArgsWS(rpcFunc *RPCFunc, params map[string]interface{}, wsCtx types.WSRPCContext) ([]reflect.Value, error) { - values, err := jsonParamsToArgs(rpcFunc, params) + values, err := jsonParamsToArgs(rpcFunc, params, 1) if err != nil { return nil, err } @@ -463,7 +468,7 @@ func (wsc *wsConnection) readRoutine() { wsCtx := types.WSRPCContext{Request: request, WSRPCConnection: wsc} args, err = jsonParamsToArgsWS(rpcFunc, request.Params, wsCtx) } else { - args, err = jsonParamsToArgs(rpcFunc, request.Params) + args, err = jsonParamsToArgs(rpcFunc, request.Params, 0) } if err != nil { wsc.WriteRPCResponse(types.NewRPCResponse(request.ID, nil, err.Error())) From db69845ded7b5e61d0ea49f58430b7f27e084ed6 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Thu, 9 Mar 2017 19:00:05 +0400 Subject: [PATCH 19/38] introduce errors pkg --- client/http_client.go | 7 ++++--- client/ws_client.go | 4 ++-- server/handlers.go | 5 +++-- server/http_params.go | 15 ++++++++------- server/http_server.go | 5 +++-- 5 files changed, 20 insertions(+), 16 deletions(-) diff --git a/client/http_client.go b/client/http_client.go index 960869b75..7df09f249 100644 --- a/client/http_client.go +++ b/client/http_client.go @@ -11,6 +11,7 @@ import ( "reflect" "strings" + "github.com/pkg/errors" types "github.com/tendermint/go-rpc/types" wire "github.com/tendermint/go-wire" ) @@ -143,16 +144,16 @@ func unmarshalResponseBytes(responseBytes []byte, result interface{}) (interface response := &types.RPCResponse{} err = json.Unmarshal(responseBytes, response) if err != nil { - return nil, fmt.Errorf("Error unmarshalling rpc response: %v", err) + return nil, errors.Errorf("Error unmarshalling rpc response: %v", err) } errorStr := response.Error if errorStr != "" { - return nil, fmt.Errorf("Response error: %v", errorStr) + return nil, errors.Errorf("Response error: %v", errorStr) } // unmarshal the RawMessage into the result result = wire.ReadJSONPtr(result, *response.Result, &err) if err != nil { - return nil, fmt.Errorf("Error unmarshalling rpc response result: %v", err) + return nil, errors.Errorf("Error unmarshalling rpc response result: %v", err) } return result, nil } diff --git a/client/ws_client.go b/client/ws_client.go index d27e499dd..b56547dd6 100644 --- a/client/ws_client.go +++ b/client/ws_client.go @@ -2,12 +2,12 @@ package rpcclient import ( "encoding/json" - "fmt" "net" "net/http" "time" "github.com/gorilla/websocket" + "github.com/pkg/errors" cmn "github.com/tendermint/go-common" types "github.com/tendermint/go-rpc/types" ) @@ -104,7 +104,7 @@ func (wsc *WSClient) receiveEventsRoutine() { continue } if response.Error != "" { - wsc.ErrorsCh <- fmt.Errorf(response.Error) + wsc.ErrorsCh <- errors.Errorf(response.Error) continue } wsc.ResultsCh <- *response.Result diff --git a/server/handlers.go b/server/handlers.go index e590a45fd..ca42b2e67 100644 --- a/server/handlers.go +++ b/server/handlers.go @@ -13,6 +13,7 @@ import ( "time" "github.com/gorilla/websocket" + "github.com/pkg/errors" cmn "github.com/tendermint/go-common" events "github.com/tendermint/go-events" types "github.com/tendermint/go-rpc/types" @@ -272,7 +273,7 @@ func nonJsonToArg(ty reflect.Type, arg string) (reflect.Value, error, bool) { if isHexString { if !expectingString && !expectingByteSlice { - err := fmt.Errorf("Got a hex string arg, but expected '%s'", + err := errors.Errorf("Got a hex string arg, but expected '%s'", ty.Kind().String()) return reflect.ValueOf(nil), err, false } @@ -567,7 +568,7 @@ func (wm *WebsocketManager) WebsocketHandler(w http.ResponseWriter, r *http.Requ func unreflectResult(returns []reflect.Value) (interface{}, error) { errV := returns[1] if errV.Interface() != nil { - return nil, fmt.Errorf("%v", errV.Interface()) + return nil, errors.Errorf("%v", errV.Interface()) } rv := returns[0] // the result is a registered interface, diff --git a/server/http_params.go b/server/http_params.go index acf5b4c8c..565060678 100644 --- a/server/http_params.go +++ b/server/http_params.go @@ -2,10 +2,11 @@ package rpcserver import ( "encoding/hex" - "fmt" "net/http" "regexp" "strconv" + + "github.com/pkg/errors" ) var ( @@ -39,7 +40,7 @@ func GetParamInt64(r *http.Request, param string) (int64, error) { s := GetParam(r, param) i, err := strconv.ParseInt(s, 10, 64) if err != nil { - return 0, fmt.Errorf(param, err.Error()) + return 0, errors.Errorf(param, err.Error()) } return i, nil } @@ -48,7 +49,7 @@ func GetParamInt32(r *http.Request, param string) (int32, error) { s := GetParam(r, param) i, err := strconv.ParseInt(s, 10, 32) if err != nil { - return 0, fmt.Errorf(param, err.Error()) + return 0, errors.Errorf(param, err.Error()) } return int32(i), nil } @@ -57,7 +58,7 @@ func GetParamUint64(r *http.Request, param string) (uint64, error) { s := GetParam(r, param) i, err := strconv.ParseUint(s, 10, 64) if err != nil { - return 0, fmt.Errorf(param, err.Error()) + return 0, errors.Errorf(param, err.Error()) } return i, nil } @@ -66,7 +67,7 @@ func GetParamUint(r *http.Request, param string) (uint, error) { s := GetParam(r, param) i, err := strconv.ParseUint(s, 10, 64) if err != nil { - return 0, fmt.Errorf(param, err.Error()) + return 0, errors.Errorf(param, err.Error()) } return uint(i), nil } @@ -74,7 +75,7 @@ func GetParamUint(r *http.Request, param string) (uint, error) { func GetParamRegexp(r *http.Request, param string, re *regexp.Regexp) (string, error) { s := GetParam(r, param) if !re.MatchString(s) { - return "", fmt.Errorf(param, "Did not match regular expression %v", re.String()) + return "", errors.Errorf(param, "Did not match regular expression %v", re.String()) } return s, nil } @@ -83,7 +84,7 @@ func GetParamFloat64(r *http.Request, param string) (float64, error) { s := GetParam(r, param) f, err := strconv.ParseFloat(s, 64) if err != nil { - return 0, fmt.Errorf(param, err.Error()) + return 0, errors.Errorf(param, err.Error()) } return f, nil } diff --git a/server/http_server.go b/server/http_server.go index 24b9f18af..5375c574f 100644 --- a/server/http_server.go +++ b/server/http_server.go @@ -11,6 +11,7 @@ import ( "strings" "time" + "github.com/pkg/errors" types "github.com/tendermint/go-rpc/types" ) @@ -24,7 +25,7 @@ func StartHTTPServer(listenAddr string, handler http.Handler) (listener net.List // TODO: Deprecate proto = types.SocketType(listenAddr) addr = listenAddr - // return nil, fmt.Errorf("Invalid listener address %s", lisenAddr) + // return nil, errors.Errorf("Invalid listener address %s", lisenAddr) } else { proto, addr = parts[0], parts[1] } @@ -32,7 +33,7 @@ func StartHTTPServer(listenAddr string, handler http.Handler) (listener net.List log.Notice(fmt.Sprintf("Starting RPC HTTP server on %s socket %v", proto, addr)) listener, err = net.Listen(proto, addr) if err != nil { - return nil, fmt.Errorf("Failed to listen to %v: %v", listenAddr, err) + return nil, errors.Errorf("Failed to listen to %v: %v", listenAddr, err) } go func() { From 715f78e26a2d64ab52569867e0fd8b3bc7a4256f Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Thu, 9 Mar 2017 21:00:25 +0100 Subject: [PATCH 20/38] Properly encode json.RawMessage --- client/http_client.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/client/http_client.go b/client/http_client.go index 7df09f249..5030323b5 100644 --- a/client/http_client.go +++ b/client/http_client.go @@ -72,7 +72,9 @@ func (c *ClientJSONRPC) Call(method string, params map[string]interface{}, resul // (handlers.go:176) on the server side encodedParams := make(map[string]interface{}) for k, v := range params { - encodedParams[k] = json.RawMessage(wire.JSONBytes(v)) + // log.Printf("%s: %v (%s)\n", k, v, string(wire.JSONBytes(v))) + bytes := json.RawMessage(wire.JSONBytes(v)) + encodedParams[k] = &bytes } request := types.RPCRequest{ JSONRPC: "2.0", @@ -84,6 +86,7 @@ func (c *ClientJSONRPC) Call(method string, params map[string]interface{}, resul if err != nil { return nil, err } + // log.Info(string(requestBytes)) requestBuf := bytes.NewBuffer(requestBytes) // log.Info(Fmt("RPC request to %v (%v): %v", c.remote, method, string(requestBytes))) httpResponse, err := c.client.Post(c.address, "text/json", requestBuf) From e6c083f589d93d5cada7c5e9fff1c3c05873a167 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Wed, 8 Mar 2017 01:22:35 +0400 Subject: [PATCH 21/38] rename ClientURI -> URIClient, ClientJSONRPC -> JSONRPCClient (Refs #4) --- README.md | 1 + client/http_client.go | 18 +++++++++--------- rpc_test.go | 18 +++++++++--------- 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 149ecdf51..06a679032 100644 --- a/README.md +++ b/README.md @@ -118,6 +118,7 @@ BREAKING CHANGES: - removed `Client` empty interface - `ClientJSONRPC#Call` `params` argument became a map +- rename `ClientURI` -> `URIClient`, `ClientJSONRPC` -> `JSONRPCClient` IMPROVEMENTS: diff --git a/client/http_client.go b/client/http_client.go index 5030323b5..96bae9d9b 100644 --- a/client/http_client.go +++ b/client/http_client.go @@ -16,7 +16,7 @@ import ( wire "github.com/tendermint/go-wire" ) -// HTTPClient is a common interface for ClientJSONRPC and ClientURI. +// HTTPClient is a common interface for JSONRPCClient and URIClient. type HTTPClient interface { Call(method string, params map[string]interface{}, result interface{}) (interface{}, error) } @@ -54,20 +54,20 @@ func makeHTTPClient(remoteAddr string) (string, *http.Client) { //------------------------------------------------------------------------------------ // JSON rpc takes params as a slice -type ClientJSONRPC struct { +type JSONRPCClient struct { address string client *http.Client } -func NewClientJSONRPC(remote string) *ClientJSONRPC { +func NewJSONRPCClient(remote string) *JSONRPCClient { address, client := makeHTTPClient(remote) - return &ClientJSONRPC{ + return &JSONRPCClient{ address: address, client: client, } } -func (c *ClientJSONRPC) Call(method string, params map[string]interface{}, result interface{}) (interface{}, error) { +func (c *JSONRPCClient) Call(method string, params map[string]interface{}, result interface{}) (interface{}, error) { // we need this step because we attempt to decode values using `go-wire` // (handlers.go:176) on the server side encodedParams := make(map[string]interface{}) @@ -105,20 +105,20 @@ func (c *ClientJSONRPC) Call(method string, params map[string]interface{}, resul //------------------------------------------------------------- // URI takes params as a map -type ClientURI struct { +type URIClient struct { address string client *http.Client } -func NewClientURI(remote string) *ClientURI { +func NewURIClient(remote string) *URIClient { address, client := makeHTTPClient(remote) - return &ClientURI{ + return &URIClient{ address: address, client: client, } } -func (c *ClientURI) Call(method string, params map[string]interface{}, result interface{}) (interface{}, error) { +func (c *URIClient) Call(method string, params map[string]interface{}, result interface{}) (interface{}, error) { values, err := argsToURLValues(params) if err != nil { return nil, err diff --git a/rpc_test.go b/rpc_test.go index 334909f1f..b1ae2566d 100644 --- a/rpc_test.go +++ b/rpc_test.go @@ -98,7 +98,7 @@ func init() { } -func testURI(t *testing.T, cl *client.ClientURI) { +func testURI(t *testing.T, cl *client.URIClient) { val := "acbd" params := map[string]interface{}{ "arg": val, @@ -114,7 +114,7 @@ func testURI(t *testing.T, cl *client.ClientURI) { } } -func testJSONRPC(t *testing.T, cl *client.ClientJSONRPC) { +func testJSONRPC(t *testing.T, cl *client.JSONRPCClient) { val := "acbd" params := map[string]interface{}{ "arg": val, @@ -164,22 +164,22 @@ func testWS(t *testing.T, cl *client.WSClient) { //------------- func TestURI_TCP(t *testing.T) { - cl := client.NewClientURI(tcpAddr) + cl := client.NewURIClient(tcpAddr) testURI(t, cl) } func TestURI_UNIX(t *testing.T) { - cl := client.NewClientURI(unixAddr) + cl := client.NewURIClient(unixAddr) testURI(t, cl) } func TestJSONRPC_TCP(t *testing.T) { - cl := client.NewClientJSONRPC(tcpAddr) + cl := client.NewJSONRPCClient(tcpAddr) testJSONRPC(t, cl) } func TestJSONRPC_UNIX(t *testing.T) { - cl := client.NewClientJSONRPC(unixAddr) + cl := client.NewJSONRPCClient(unixAddr) testJSONRPC(t, cl) } @@ -202,7 +202,7 @@ func TestWS_UNIX(t *testing.T) { } func TestHexStringArg(t *testing.T) { - cl := client.NewClientURI(tcpAddr) + cl := client.NewURIClient(tcpAddr) // should NOT be handled as hex val := "0xabc" params := map[string]interface{}{ @@ -220,7 +220,7 @@ func TestHexStringArg(t *testing.T) { } func TestQuotedStringArg(t *testing.T) { - cl := client.NewClientURI(tcpAddr) + cl := client.NewURIClient(tcpAddr) // should NOT be unquoted val := "\"abc\"" params := map[string]interface{}{ @@ -248,7 +248,7 @@ func randBytes(t *testing.T) []byte { } func TestByteSliceViaJSONRPC(t *testing.T) { - cl := client.NewClientJSONRPC(unixAddr) + cl := client.NewJSONRPCClient(unixAddr) val := randBytes(t) params := map[string]interface{}{ From d66ebbd90407d22b52a74711d21073409a91c571 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Fri, 10 Mar 2017 12:03:16 +0400 Subject: [PATCH 22/38] use testify package --- Makefile | 3 ++ rpc_test.go | 82 ++++++++++++++--------------------------------------- 2 files changed, 25 insertions(+), 60 deletions(-) diff --git a/Makefile b/Makefile index a2e3bea7f..0937558a8 100644 --- a/Makefile +++ b/Makefile @@ -11,5 +11,8 @@ test: get_deps: @echo "--> Running go get" @go get -v -d $(PACKAGES) + @go list -f '{{join .TestImports "\n"}}' ./... | \ + grep -v /vendor/ | sort | uniq | \ + xargs go get -v -d .PHONY: all test get_deps diff --git a/rpc_test.go b/rpc_test.go index b1ae2566d..b52794ada 100644 --- a/rpc_test.go +++ b/rpc_test.go @@ -9,6 +9,8 @@ import ( "testing" "time" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" client "github.com/tendermint/go-rpc/client" server "github.com/tendermint/go-rpc/server" types "github.com/tendermint/go-rpc/types" @@ -105,13 +107,9 @@ func testURI(t *testing.T, cl *client.URIClient) { } var result Result _, err := cl.Call("status", params, &result) - if err != nil { - t.Fatal(err) - } + require.Nil(t, err) got := result.(*ResultStatus).Value - if got != val { - t.Fatalf("Got: %v .... Expected: %v \n", got, val) - } + assert.Equal(t, got, val) } func testJSONRPC(t *testing.T, cl *client.JSONRPCClient) { @@ -121,13 +119,9 @@ func testJSONRPC(t *testing.T, cl *client.JSONRPCClient) { } var result Result _, err := cl.Call("status", params, &result) - if err != nil { - t.Fatal(err) - } + require.Nil(t, err) got := result.(*ResultStatus).Value - if got != val { - t.Fatalf("Got: %v .... Expected: %v \n", got, val) - } + assert.Equal(t, got, val) } func testWS(t *testing.T, cl *client.WSClient) { @@ -141,21 +135,15 @@ func testWS(t *testing.T, cl *client.WSClient) { Method: "status", Params: params, }) - if err != nil { - t.Fatal(err) - } + require.Nil(t, err) select { case msg := <-cl.ResultsCh: result := new(Result) wire.ReadJSONPtr(result, msg, &err) - if err != nil { - t.Fatal(err) - } + require.Nil(t, err) got := (*result).(*ResultStatus).Value - if got != val { - t.Fatalf("Got: %v .... Expected: %v \n", got, val) - } + assert.Equal(t, got, val) case err := <-cl.ErrorsCh: t.Fatal(err) } @@ -186,18 +174,14 @@ func TestJSONRPC_UNIX(t *testing.T) { func TestWS_TCP(t *testing.T) { cl := client.NewWSClient(tcpAddr, websocketEndpoint) _, err := cl.Start() - if err != nil { - t.Fatal(err) - } + require.Nil(t, err) testWS(t, cl) } func TestWS_UNIX(t *testing.T) { cl := client.NewWSClient(unixAddr, websocketEndpoint) _, err := cl.Start() - if err != nil { - t.Fatal(err) - } + require.Nil(t, err) testWS(t, cl) } @@ -210,13 +194,9 @@ func TestHexStringArg(t *testing.T) { } var result Result _, err := cl.Call("status", params, &result) - if err != nil { - t.Fatal(err) - } + require.Nil(t, err) got := result.(*ResultStatus).Value - if got != val { - t.Fatalf("Got: %v .... Expected: %v \n", got, val) - } + assert.Equal(t, got, val) } func TestQuotedStringArg(t *testing.T) { @@ -228,22 +208,16 @@ func TestQuotedStringArg(t *testing.T) { } var result Result _, err := cl.Call("status", params, &result) - if err != nil { - t.Fatal(err) - } + require.Nil(t, err) got := result.(*ResultStatus).Value - if got != val { - t.Fatalf("Got: %v .... Expected: %v \n", got, val) - } + assert.Equal(t, got, val) } func randBytes(t *testing.T) []byte { n := rand.Intn(10) + 2 buf := make([]byte, n) _, err := crand.Read(buf) - if err != nil { - t.Fatal(err) - } + require.Nil(t, err) return bytes.Replace(buf, []byte("="), []byte{100}, -1) } @@ -256,21 +230,15 @@ func TestByteSliceViaJSONRPC(t *testing.T) { } var result Result _, err := cl.Call("bytes", params, &result) - if err != nil { - t.Fatal(err) - } + require.Nil(t, err) got := result.(*ResultBytes).Value - if bytes.Compare(got, val) != 0 { - t.Fatalf("Got: %v .... Expected: %v \n", got, val) - } + assert.Equal(t, got, val) } func TestWSNewWSRPCFunc(t *testing.T) { cl := client.NewWSClient(unixAddr, websocketEndpoint) _, err := cl.Start() - if err != nil { - t.Fatal(err) - } + require.Nil(t, err) defer cl.Stop() val := "acbd" @@ -283,21 +251,15 @@ func TestWSNewWSRPCFunc(t *testing.T) { Method: "status_ws", Params: params, }) - if err != nil { - t.Fatal(err) - } + require.Nil(t, err) select { case msg := <-cl.ResultsCh: result := new(Result) wire.ReadJSONPtr(result, msg, &err) - if err != nil { - t.Fatal(err) - } + require.Nil(t, err) got := (*result).(*ResultStatus).Value - if got != val { - t.Fatalf("Got: %v .... Expected: %v \n", got, val) - } + assert.Equal(t, got, val) case err := <-cl.ErrorsCh: t.Fatal(err) } From 0874c72819f0b78ded043e05a4cdce3b973230e0 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Fri, 10 Mar 2017 12:52:40 +0400 Subject: [PATCH 23/38] refactor tests --- rpc_test.go | 83 +++++++++++++++++------------------------------------ 1 file changed, 27 insertions(+), 56 deletions(-) diff --git a/rpc_test.go b/rpc_test.go index b52794ada..acbf440d0 100644 --- a/rpc_test.go +++ b/rpc_test.go @@ -3,6 +3,7 @@ package rpc import ( "bytes" crand "crypto/rand" + "fmt" "math/rand" "net/http" "os/exec" @@ -100,31 +101,25 @@ func init() { } -func testURI(t *testing.T, cl *client.URIClient) { - val := "acbd" +func status(cl client.HTTPClient, val string) (string, error) { params := map[string]interface{}{ "arg": val, } var result Result - _, err := cl.Call("status", params, &result) - require.Nil(t, err) - got := result.(*ResultStatus).Value - assert.Equal(t, got, val) + if _, err := cl.Call("status", params, &result); err != nil { + return "", err + } + return result.(*ResultStatus).Value, nil } -func testJSONRPC(t *testing.T, cl *client.JSONRPCClient) { +func testWithHTTPClient(t *testing.T, cl client.HTTPClient) { val := "acbd" - params := map[string]interface{}{ - "arg": val, - } - var result Result - _, err := cl.Call("status", params, &result) + got, err := status(cl, val) require.Nil(t, err) - got := result.(*ResultStatus).Value assert.Equal(t, got, val) } -func testWS(t *testing.T, cl *client.WSClient) { +func testWithWSClient(t *testing.T, cl *client.WSClient) { val := "acbd" params := map[string]interface{}{ "arg": val, @@ -151,51 +146,32 @@ func testWS(t *testing.T, cl *client.WSClient) { //------------- -func TestURI_TCP(t *testing.T) { - cl := client.NewURIClient(tcpAddr) - testURI(t, cl) -} - -func TestURI_UNIX(t *testing.T) { - cl := client.NewURIClient(unixAddr) - testURI(t, cl) -} - -func TestJSONRPC_TCP(t *testing.T) { - cl := client.NewJSONRPCClient(tcpAddr) - testJSONRPC(t, cl) -} +func TestServersAndClientsBasic(t *testing.T) { + serverAddrs := [...]string{tcpAddr, unixAddr} + for _, addr := range serverAddrs { + cl1 := client.NewURIClient(addr) + fmt.Printf("=== testing server on %s using %v client", addr, cl1) + testWithHTTPClient(t, cl1) -func TestJSONRPC_UNIX(t *testing.T) { - cl := client.NewJSONRPCClient(unixAddr) - testJSONRPC(t, cl) -} + cl2 := client.NewJSONRPCClient(tcpAddr) + fmt.Printf("=== testing server on %s using %v client", addr, cl2) + testWithHTTPClient(t, cl2) -func TestWS_TCP(t *testing.T) { - cl := client.NewWSClient(tcpAddr, websocketEndpoint) - _, err := cl.Start() - require.Nil(t, err) - testWS(t, cl) -} - -func TestWS_UNIX(t *testing.T) { - cl := client.NewWSClient(unixAddr, websocketEndpoint) - _, err := cl.Start() - require.Nil(t, err) - testWS(t, cl) + cl3 := client.NewWSClient(tcpAddr, websocketEndpoint) + _, err := cl3.Start() + require.Nil(t, err) + fmt.Printf("=== testing server on %s using %v client", addr, cl3) + testWithWSClient(t, cl3) + cl3.Stop() + } } func TestHexStringArg(t *testing.T) { cl := client.NewURIClient(tcpAddr) // should NOT be handled as hex val := "0xabc" - params := map[string]interface{}{ - "arg": val, - } - var result Result - _, err := cl.Call("status", params, &result) + got, err := status(cl, val) require.Nil(t, err) - got := result.(*ResultStatus).Value assert.Equal(t, got, val) } @@ -203,13 +179,8 @@ func TestQuotedStringArg(t *testing.T) { cl := client.NewURIClient(tcpAddr) // should NOT be unquoted val := "\"abc\"" - params := map[string]interface{}{ - "arg": val, - } - var result Result - _, err := cl.Call("status", params, &result) + got, err := status(cl, val) require.Nil(t, err) - got := result.(*ResultStatus).Value assert.Equal(t, got, val) } From c88257b0384f6ceeb1219c527cb0ce06402acb1d Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Fri, 10 Mar 2017 12:57:14 +0400 Subject: [PATCH 24/38] rename rpc function status to echo echo means we're returning the input, which is exactly what this function does. --- rpc_test.go | 51 +++++++++++++++++++++++++-------------------------- 1 file changed, 25 insertions(+), 26 deletions(-) diff --git a/rpc_test.go b/rpc_test.go index acbf440d0..7b043953c 100644 --- a/rpc_test.go +++ b/rpc_test.go @@ -31,38 +31,37 @@ const ( // Define a type for results and register concrete versions type Result interface{} -type ResultStatus struct { +type ResultEcho struct { Value string } -type ResultBytes struct { +type ResultEchoBytes struct { Value []byte } var _ = wire.RegisterInterface( struct{ Result }{}, - wire.ConcreteType{&ResultStatus{}, 0x1}, - wire.ConcreteType{&ResultBytes{}, 0x2}, + wire.ConcreteType{&ResultEcho{}, 0x1}, + wire.ConcreteType{&ResultEchoBytes{}, 0x2}, ) // Define some routes var Routes = map[string]*server.RPCFunc{ - "status": server.NewRPCFunc(StatusResult, "arg"), - "status_ws": server.NewWSRPCFunc(StatusWSResult, "arg"), - "bytes": server.NewRPCFunc(BytesResult, "arg"), + "echo": server.NewRPCFunc(EchoResult, "arg"), + "echo_ws": server.NewWSRPCFunc(EchoWSResult, "arg"), + "echo_bytes": server.NewRPCFunc(EchoBytesResult, "arg"), } -// an rpc function -func StatusResult(v string) (Result, error) { - return &ResultStatus{v}, nil +func EchoResult(v string) (Result, error) { + return &ResultEcho{v}, nil } -func StatusWSResult(wsCtx types.WSRPCContext, v string) (Result, error) { - return &ResultStatus{v}, nil +func EchoWSResult(wsCtx types.WSRPCContext, v string) (Result, error) { + return &ResultEcho{v}, nil } -func BytesResult(v []byte) (Result, error) { - return &ResultBytes{v}, nil +func EchoBytesResult(v []byte) (Result, error) { + return &ResultEchoBytes{v}, nil } // launch unix and tcp servers @@ -101,20 +100,20 @@ func init() { } -func status(cl client.HTTPClient, val string) (string, error) { +func echo(cl client.HTTPClient, val string) (string, error) { params := map[string]interface{}{ "arg": val, } var result Result - if _, err := cl.Call("status", params, &result); err != nil { + if _, err := cl.Call("echo", params, &result); err != nil { return "", err } - return result.(*ResultStatus).Value, nil + return result.(*ResultEcho).Value, nil } func testWithHTTPClient(t *testing.T, cl client.HTTPClient) { val := "acbd" - got, err := status(cl, val) + got, err := echo(cl, val) require.Nil(t, err) assert.Equal(t, got, val) } @@ -127,7 +126,7 @@ func testWithWSClient(t *testing.T, cl *client.WSClient) { err := cl.WriteJSON(types.RPCRequest{ JSONRPC: "2.0", ID: "", - Method: "status", + Method: "echo", Params: params, }) require.Nil(t, err) @@ -137,7 +136,7 @@ func testWithWSClient(t *testing.T, cl *client.WSClient) { result := new(Result) wire.ReadJSONPtr(result, msg, &err) require.Nil(t, err) - got := (*result).(*ResultStatus).Value + got := (*result).(*ResultEcho).Value assert.Equal(t, got, val) case err := <-cl.ErrorsCh: t.Fatal(err) @@ -170,7 +169,7 @@ func TestHexStringArg(t *testing.T) { cl := client.NewURIClient(tcpAddr) // should NOT be handled as hex val := "0xabc" - got, err := status(cl, val) + got, err := echo(cl, val) require.Nil(t, err) assert.Equal(t, got, val) } @@ -179,7 +178,7 @@ func TestQuotedStringArg(t *testing.T) { cl := client.NewURIClient(tcpAddr) // should NOT be unquoted val := "\"abc\"" - got, err := status(cl, val) + got, err := echo(cl, val) require.Nil(t, err) assert.Equal(t, got, val) } @@ -200,9 +199,9 @@ func TestByteSliceViaJSONRPC(t *testing.T) { "arg": val, } var result Result - _, err := cl.Call("bytes", params, &result) + _, err := cl.Call("echo_bytes", params, &result) require.Nil(t, err) - got := result.(*ResultBytes).Value + got := result.(*ResultEchoBytes).Value assert.Equal(t, got, val) } @@ -219,7 +218,7 @@ func TestWSNewWSRPCFunc(t *testing.T) { err = cl.WriteJSON(types.RPCRequest{ JSONRPC: "2.0", ID: "", - Method: "status_ws", + Method: "echo_ws", Params: params, }) require.Nil(t, err) @@ -229,7 +228,7 @@ func TestWSNewWSRPCFunc(t *testing.T) { result := new(Result) wire.ReadJSONPtr(result, msg, &err) require.Nil(t, err) - got := (*result).(*ResultStatus).Value + got := (*result).(*ResultEcho).Value assert.Equal(t, got, val) case err := <-cl.ErrorsCh: t.Fatal(err) From 3233c9c003b8ed81ce2e9d18d39fdb5164d2d62a Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Fri, 10 Mar 2017 14:56:04 +0400 Subject: [PATCH 25/38] WSClient failed to "echo_bytes" Error: ``` Expected nil, but got: encoding/hex: invalid byte: U+0078 'x' ``` --- rpc_test.go | 112 ++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 77 insertions(+), 35 deletions(-) diff --git a/rpc_test.go b/rpc_test.go index 7b043953c..41952fca4 100644 --- a/rpc_test.go +++ b/rpc_test.go @@ -97,10 +97,9 @@ func init() { // wait for servers to start time.Sleep(time.Second * 2) - } -func echo(cl client.HTTPClient, val string) (string, error) { +func echoViaHTTP(cl client.HTTPClient, val string) (string, error) { params := map[string]interface{}{ "arg": val, } @@ -111,15 +110,30 @@ func echo(cl client.HTTPClient, val string) (string, error) { return result.(*ResultEcho).Value, nil } +func echoBytesViaHTTP(cl client.HTTPClient, bytes []byte) ([]byte, error) { + params := map[string]interface{}{ + "arg": bytes, + } + var result Result + if _, err := cl.Call("echo_bytes", params, &result); err != nil { + return []byte{}, err + } + return result.(*ResultEchoBytes).Value, nil +} + func testWithHTTPClient(t *testing.T, cl client.HTTPClient) { val := "acbd" - got, err := echo(cl, val) + got, err := echoViaHTTP(cl, val) require.Nil(t, err) assert.Equal(t, got, val) + + val2 := randBytes(t) + got2, err := echoBytesViaHTTP(cl, val2) + require.Nil(t, err) + assert.Equal(t, got2, val2) } -func testWithWSClient(t *testing.T, cl *client.WSClient) { - val := "acbd" +func echoViaWS(cl *client.WSClient, val string) (string, error) { params := map[string]interface{}{ "arg": val, } @@ -129,20 +143,62 @@ func testWithWSClient(t *testing.T, cl *client.WSClient) { Method: "echo", Params: params, }) - require.Nil(t, err) + if err != nil { + return "", err + } select { case msg := <-cl.ResultsCh: result := new(Result) wire.ReadJSONPtr(result, msg, &err) - require.Nil(t, err) - got := (*result).(*ResultEcho).Value - assert.Equal(t, got, val) + if err != nil { + return "", nil + } + return (*result).(*ResultEcho).Value, nil case err := <-cl.ErrorsCh: - t.Fatal(err) + return "", err } } +func echoBytesViaWS(cl *client.WSClient, bytes []byte) ([]byte, error) { + params := map[string]interface{}{ + "arg": bytes, + } + err := cl.WriteJSON(types.RPCRequest{ + JSONRPC: "2.0", + ID: "", + Method: "echo_bytes", + Params: params, + }) + if err != nil { + return []byte{}, err + } + + select { + case msg := <-cl.ResultsCh: + result := new(Result) + wire.ReadJSONPtr(result, msg, &err) + if err != nil { + return []byte{}, nil + } + return (*result).(*ResultEchoBytes).Value, nil + case err := <-cl.ErrorsCh: + return []byte{}, err + } +} + +func testWithWSClient(t *testing.T, cl *client.WSClient) { + val := "acbd" + got, err := echoViaWS(cl, val) + require.Nil(t, err) + assert.Equal(t, got, val) + + val2 := randBytes(t) + got2, err := echoBytesViaWS(cl, val2) + require.Nil(t, err) + assert.Equal(t, got2, val2) +} + //------------- func TestServersAndClientsBasic(t *testing.T) { @@ -169,7 +225,7 @@ func TestHexStringArg(t *testing.T) { cl := client.NewURIClient(tcpAddr) // should NOT be handled as hex val := "0xabc" - got, err := echo(cl, val) + got, err := echoViaHTTP(cl, val) require.Nil(t, err) assert.Equal(t, got, val) } @@ -178,35 +234,13 @@ func TestQuotedStringArg(t *testing.T) { cl := client.NewURIClient(tcpAddr) // should NOT be unquoted val := "\"abc\"" - got, err := echo(cl, val) + got, err := echoViaHTTP(cl, val) require.Nil(t, err) assert.Equal(t, got, val) } -func randBytes(t *testing.T) []byte { - n := rand.Intn(10) + 2 - buf := make([]byte, n) - _, err := crand.Read(buf) - require.Nil(t, err) - return bytes.Replace(buf, []byte("="), []byte{100}, -1) -} - -func TestByteSliceViaJSONRPC(t *testing.T) { - cl := client.NewJSONRPCClient(unixAddr) - - val := randBytes(t) - params := map[string]interface{}{ - "arg": val, - } - var result Result - _, err := cl.Call("echo_bytes", params, &result) - require.Nil(t, err) - got := result.(*ResultEchoBytes).Value - assert.Equal(t, got, val) -} - func TestWSNewWSRPCFunc(t *testing.T) { - cl := client.NewWSClient(unixAddr, websocketEndpoint) + cl := client.NewWSClient(tcpAddr, websocketEndpoint) _, err := cl.Start() require.Nil(t, err) defer cl.Stop() @@ -234,3 +268,11 @@ func TestWSNewWSRPCFunc(t *testing.T) { t.Fatal(err) } } + +func randBytes(t *testing.T) []byte { + n := rand.Intn(10) + 2 + buf := make([]byte, n) + _, err := crand.Read(buf) + require.Nil(t, err) + return bytes.Replace(buf, []byte("="), []byte{100}, -1) +} From 5d19a008ce4ea633b2002404df1e6b6be298f886 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Fri, 10 Mar 2017 15:23:43 +0400 Subject: [PATCH 26/38] add Call method to WSClient, which does proper encoding of params --- README.md | 1 + client/http_client.go | 1 - client/ws_client.go | 26 ++++++++++++++++++++++++-- rpc_test.go | 14 ++------------ 4 files changed, 27 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 06a679032..5928e6fe0 100644 --- a/README.md +++ b/README.md @@ -125,3 +125,4 @@ IMPROVEMENTS: - added `HTTPClient` interface, which can be used for both `ClientURI` and `ClientJSONRPC` - all params are now optional (Golang's default will be used if some param is missing) +- added `Call` method to `WSClient` (see method's doc for details) diff --git a/client/http_client.go b/client/http_client.go index 96bae9d9b..f4a2a6d7e 100644 --- a/client/http_client.go +++ b/client/http_client.go @@ -72,7 +72,6 @@ func (c *JSONRPCClient) Call(method string, params map[string]interface{}, resul // (handlers.go:176) on the server side encodedParams := make(map[string]interface{}) for k, v := range params { - // log.Printf("%s: %v (%s)\n", k, v, string(wire.JSONBytes(v))) bytes := json.RawMessage(wire.JSONBytes(v)) encodedParams[k] = &bytes } diff --git a/client/ws_client.go b/client/ws_client.go index b56547dd6..ecf641221 100644 --- a/client/ws_client.go +++ b/client/ws_client.go @@ -10,6 +10,7 @@ import ( "github.com/pkg/errors" cmn "github.com/tendermint/go-common" types "github.com/tendermint/go-rpc/types" + wire "github.com/tendermint/go-wire" ) const ( @@ -116,7 +117,8 @@ func (wsc *WSClient) receiveEventsRoutine() { close(wsc.ErrorsCh) } -// subscribe to an event +// Subscribe to an event. Note the server must have a "subscribe" route +// defined. func (wsc *WSClient) Subscribe(eventid string) error { err := wsc.WriteJSON(types.RPCRequest{ JSONRPC: "2.0", @@ -127,7 +129,8 @@ func (wsc *WSClient) Subscribe(eventid string) error { return err } -// unsubscribe from an event +// Unsubscribe from an event. Note the server must have a "unsubscribe" route +// defined. func (wsc *WSClient) Unsubscribe(eventid string) error { err := wsc.WriteJSON(types.RPCRequest{ JSONRPC: "2.0", @@ -137,3 +140,22 @@ func (wsc *WSClient) Unsubscribe(eventid string) error { }) return err } + +// Call asynchronously calls a given method by sending an RPCRequest to the +// server. Results will be available on ResultsCh, errors, if any, on ErrorsCh. +func (wsc *WSClient) Call(method string, params map[string]interface{}) error { + // we need this step because we attempt to decode values using `go-wire` + // (handlers.go:470) on the server side + encodedParams := make(map[string]interface{}) + for k, v := range params { + bytes := json.RawMessage(wire.JSONBytes(v)) + encodedParams[k] = &bytes + } + err := wsc.WriteJSON(types.RPCRequest{ + JSONRPC: "2.0", + Method: method, + Params: encodedParams, + ID: "", + }) + return err +} diff --git a/rpc_test.go b/rpc_test.go index 41952fca4..8a05d7295 100644 --- a/rpc_test.go +++ b/rpc_test.go @@ -137,12 +137,7 @@ func echoViaWS(cl *client.WSClient, val string) (string, error) { params := map[string]interface{}{ "arg": val, } - err := cl.WriteJSON(types.RPCRequest{ - JSONRPC: "2.0", - ID: "", - Method: "echo", - Params: params, - }) + err := cl.Call("echo", params) if err != nil { return "", err } @@ -164,12 +159,7 @@ func echoBytesViaWS(cl *client.WSClient, bytes []byte) ([]byte, error) { params := map[string]interface{}{ "arg": bytes, } - err := cl.WriteJSON(types.RPCRequest{ - JSONRPC: "2.0", - ID: "", - Method: "echo_bytes", - Params: params, - }) + err := cl.Call("echo_bytes", params) if err != nil { return []byte{}, err } From b54b9b4ecc2c444f7b11077f24e5abafdcc98676 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Mon, 13 Mar 2017 14:25:57 +0400 Subject: [PATCH 27/38] update url to network monitor [ci skip] [circleci skip] --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 06a679032..cb869d517 100644 --- a/README.md +++ b/README.md @@ -108,7 +108,7 @@ Each route is available as a GET request, as a JSONRPCv2 POST request, and via J # Examples * [Tendermint](https://github.com/tendermint/tendermint/blob/master/rpc/core/routes.go) -* [Network Monitor](https://github.com/tendermint/netmon/blob/master/handlers/routes.go) +* [tm-monitor](https://github.com/tendermint/tools/blob/master/tm-monitor/rpc.go) ## CHANGELOG From afc39febed0c51e01ba2474980533a5c5a899258 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Tue, 21 Mar 2017 20:47:12 +0400 Subject: [PATCH 28/38] close ws connection on Stop --- client/ws_client.go | 2 ++ server/handlers.go | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/client/ws_client.go b/client/ws_client.go index b56547dd6..9ed2be8c5 100644 --- a/client/ws_client.go +++ b/client/ws_client.go @@ -85,6 +85,8 @@ func (wsc *WSClient) dial() error { func (wsc *WSClient) OnStop() { wsc.BaseService.OnStop() + wsc.Conn.Close() + wsc.Conn = nil // ResultsCh/ErrorsCh is closed in receiveEventsRoutine. } diff --git a/server/handlers.go b/server/handlers.go index ca42b2e67..5b6008504 100644 --- a/server/handlers.go +++ b/server/handlers.go @@ -373,7 +373,9 @@ func (wsc *wsConnection) OnStart() error { func (wsc *wsConnection) OnStop() { wsc.BaseService.OnStop() - wsc.evsw.RemoveListener(wsc.remoteAddr) + if wsc.evsw != nil { + wsc.evsw.RemoveListener(wsc.remoteAddr) + } wsc.readTimeout.Stop() wsc.pingTicker.Stop() // The write loop closes the websocket connection From d6587be7bcacf7cdc5a547f8a2f3862b3d7bd8df Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Tue, 21 Mar 2017 22:05:53 +0400 Subject: [PATCH 29/38] [WSClient] allow for multiple restarts needed for https://github.com/tendermint/tools/pull/13/commits/3044f66ba90694927fb22ea5267de2a90bb3281b See https://github.com/tendermint/tools/issues/6 --- client/ws_client.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/client/ws_client.go b/client/ws_client.go index b56547dd6..99737ca8f 100644 --- a/client/ws_client.go +++ b/client/ws_client.go @@ -53,6 +53,9 @@ func (wsc *WSClient) OnStart() error { if err != nil { return err } + + wsc.ResultsCh = make(chan json.RawMessage, wsResultsChannelCapacity) + wsc.ErrorsCh = make(chan error, wsErrorsChannelCapacity) go wsc.receiveEventsRoutine() return nil } From b0d2032488a16ce1497edd47e7c2e0dda246723f Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Tue, 28 Mar 2017 14:01:22 +0400 Subject: [PATCH 30/38] use BaseService.OnReset method to recreate channels --- client/ws_client.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/client/ws_client.go b/client/ws_client.go index 99737ca8f..fb359bdaf 100644 --- a/client/ws_client.go +++ b/client/ws_client.go @@ -47,16 +47,21 @@ func (wsc *WSClient) String() string { return wsc.Address + ", " + wsc.Endpoint } +// OnStart implements cmn.BaseService interface func (wsc *WSClient) OnStart() error { wsc.BaseService.OnStart() err := wsc.dial() if err != nil { return err } + go wsc.receiveEventsRoutine() + return nil +} +// OnReset implements cmn.BaseService interface +func (wsc *WSClient) OnReset() error { wsc.ResultsCh = make(chan json.RawMessage, wsResultsChannelCapacity) wsc.ErrorsCh = make(chan error, wsErrorsChannelCapacity) - go wsc.receiveEventsRoutine() return nil } @@ -86,6 +91,7 @@ func (wsc *WSClient) dial() error { return nil } +// OnStop implements cmn.BaseService interface func (wsc *WSClient) OnStop() { wsc.BaseService.OnStop() // ResultsCh/ErrorsCh is closed in receiveEventsRoutine. From ba5382b70e65f89cd771157fc900cfc6adb22819 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Tue, 28 Mar 2017 14:17:40 +0400 Subject: [PATCH 31/38] open result&error channels on start --- client/ws_client.go | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/client/ws_client.go b/client/ws_client.go index fb359bdaf..8fa6189d1 100644 --- a/client/ws_client.go +++ b/client/ws_client.go @@ -32,12 +32,10 @@ type WSClient struct { func NewWSClient(remoteAddr, endpoint string) *WSClient { addr, dialer := makeHTTPDialer(remoteAddr) wsClient := &WSClient{ - Address: addr, - Dialer: dialer, - Endpoint: endpoint, - Conn: nil, - ResultsCh: make(chan json.RawMessage, wsResultsChannelCapacity), - ErrorsCh: make(chan error, wsErrorsChannelCapacity), + Address: addr, + Dialer: dialer, + Endpoint: endpoint, + Conn: nil, } wsClient.BaseService = *cmn.NewBaseService(log, "WSClient", wsClient) return wsClient @@ -54,14 +52,14 @@ func (wsc *WSClient) OnStart() error { if err != nil { return err } + wsc.ResultsCh = make(chan json.RawMessage, wsResultsChannelCapacity) + wsc.ErrorsCh = make(chan error, wsErrorsChannelCapacity) go wsc.receiveEventsRoutine() return nil } // OnReset implements cmn.BaseService interface func (wsc *WSClient) OnReset() error { - wsc.ResultsCh = make(chan json.RawMessage, wsResultsChannelCapacity) - wsc.ErrorsCh = make(chan error, wsErrorsChannelCapacity) return nil } From 9d18cbe74e66f875afa36d2fa3be280e4a2dc9e6 Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Tue, 11 Apr 2017 13:29:49 +0200 Subject: [PATCH 32/38] Remove race condition between read go-routine and stop --- client/ws_client.go | 4 +++- server/handlers.go | 7 +++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/client/ws_client.go b/client/ws_client.go index 9ed2be8c5..9933ab073 100644 --- a/client/ws_client.go +++ b/client/ws_client.go @@ -86,7 +86,6 @@ func (wsc *WSClient) dial() error { func (wsc *WSClient) OnStop() { wsc.BaseService.OnStop() wsc.Conn.Close() - wsc.Conn = nil // ResultsCh/ErrorsCh is closed in receiveEventsRoutine. } @@ -112,6 +111,9 @@ func (wsc *WSClient) receiveEventsRoutine() { wsc.ResultsCh <- *response.Result } } + // this must be modified in the same go-routine that reads from the + // connection to avoid race conditions + wsc.Conn = nil // Cleanup close(wsc.ResultsCh) diff --git a/server/handlers.go b/server/handlers.go index 5b6008504..573696fb6 100644 --- a/server/handlers.go +++ b/server/handlers.go @@ -347,12 +347,15 @@ func NewWSConnection(baseConn *websocket.Conn, funcMap map[string]*RPCFunc, evsw func (wsc *wsConnection) OnStart() error { wsc.BaseService.OnStart() + // these must be set before the readRoutine is created, as it may + // call wsc.Stop(), which accesses these timers + wsc.readTimeout = time.NewTimer(time.Second * wsReadTimeoutSeconds) + wsc.pingTicker = time.NewTicker(time.Second * wsPingTickerSeconds) + // Read subscriptions/unsubscriptions to events go wsc.readRoutine() // Custom Ping handler to touch readTimeout - wsc.readTimeout = time.NewTimer(time.Second * wsReadTimeoutSeconds) - wsc.pingTicker = time.NewTicker(time.Second * wsPingTickerSeconds) wsc.baseConn.SetPingHandler(func(m string) error { // NOTE: https://github.com/gorilla/websocket/issues/97 go wsc.baseConn.WriteControl(websocket.PongMessage, []byte(m), time.Now().Add(time.Second*wsWriteTimeoutSeconds)) From c3295f4878019ff3fdfcac37a4c0e4bcf4bb02a7 Mon Sep 17 00:00:00 2001 From: Ethan Buchman Date: Wed, 12 Apr 2017 13:42:19 -0400 Subject: [PATCH 33/38] RPCRequest.Params can be map[string]interface{} or []interface{} --- server/handlers.go | 44 ++++++++++++++++++++++++++++++++------------ types/types.go | 8 ++++---- 2 files changed, 36 insertions(+), 16 deletions(-) diff --git a/server/handlers.go b/server/handlers.go index 573696fb6..051d67d00 100644 --- a/server/handlers.go +++ b/server/handlers.go @@ -140,36 +140,56 @@ func makeJSONRPCHandler(funcMap map[string]*RPCFunc) http.HandlerFunc { } } -// Convert a list of interfaces to properly typed values +// Convert a []interface{} OR a map[string]interface{} to properly typed values // // argsOffset is used in jsonParamsToArgsWS, where len(rpcFunc.args) != len(rpcFunc.argNames). // Example: // rpcFunc.args = [rpctypes.WSRPCContext string] // rpcFunc.argNames = ["arg"] -func jsonParamsToArgs(rpcFunc *RPCFunc, params map[string]interface{}, argsOffset int) ([]reflect.Value, error) { +func jsonParamsToArgs(rpcFunc *RPCFunc, paramsI interface{}, argsOffset int) ([]reflect.Value, error) { values := make([]reflect.Value, len(rpcFunc.argNames)) - for i, argName := range rpcFunc.argNames { - argType := rpcFunc.args[i+argsOffset] + switch params := paramsI.(type) { - // decode param if provided - if param, ok := params[argName]; ok && "" != param { - v, err := _jsonObjectToArg(argType, param) + case map[string]interface{}: + for i, argName := range rpcFunc.argNames { + argType := rpcFunc.args[i+argsOffset] + + // decode param if provided + if param, ok := params[argName]; ok && "" != param { + v, err := _jsonObjectToArg(argType, param) + if err != nil { + return nil, err + } + values[i] = v + } else { // use default for that type + values[i] = reflect.Zero(argType) + } + } + case []interface{}: + if len(rpcFunc.argNames) != len(params) { + return nil, errors.New(fmt.Sprintf("Expected %v parameters (%v), got %v (%v)", + len(rpcFunc.argNames), rpcFunc.argNames, len(params), params)) + } + values := make([]reflect.Value, len(params)) + for i, p := range params { + ty := rpcFunc.args[i] + v, err := _jsonObjectToArg(ty, p) if err != nil { return nil, err } values[i] = v - } else { // use default for that type - values[i] = reflect.Zero(argType) } + return values, nil + default: + return nil, fmt.Errorf("Unknown type for JSON params %v. Expected map[string]interface{} or []interface{}", reflect.TypeOf(paramsI)) } - return values, nil } // Same as above, but with the first param the websocket connection -func jsonParamsToArgsWS(rpcFunc *RPCFunc, params map[string]interface{}, wsCtx types.WSRPCContext) ([]reflect.Value, error) { - values, err := jsonParamsToArgs(rpcFunc, params, 1) +func jsonParamsToArgsWS(rpcFunc *RPCFunc, paramsI interface{}, wsCtx types.WSRPCContext) ([]reflect.Value, error) { + values, err := jsonParamsToArgs(rpcFunc, paramsI, 1) if err != nil { return nil, err } diff --git a/types/types.go b/types/types.go index cebd7564a..38c7f09db 100644 --- a/types/types.go +++ b/types/types.go @@ -9,10 +9,10 @@ import ( ) type RPCRequest struct { - JSONRPC string `json:"jsonrpc"` - ID string `json:"id"` - Method string `json:"method"` - Params map[string]interface{} `json:"params"` + JSONRPC string `json:"jsonrpc"` + ID string `json:"id"` + Method string `json:"method"` + Params interface{} `json:"params"` // must be map[string]interface{} or []interface{} } func NewRPCRequest(id string, method string, params map[string]interface{}) RPCRequest { From 8c385433576988ab4b0269a8220d095d82b96e6e Mon Sep 17 00:00:00 2001 From: Ethan Buchman Date: Wed, 12 Apr 2017 18:15:51 -0400 Subject: [PATCH 34/38] fix error msg --- server/handlers.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/handlers.go b/server/handlers.go index 051d67d00..de2b8094f 100644 --- a/server/handlers.go +++ b/server/handlers.go @@ -133,7 +133,7 @@ func makeJSONRPCHandler(funcMap map[string]*RPCFunc) http.HandlerFunc { log.Info("HTTPJSONRPC", "method", request.Method, "args", args, "returns", returns) result, err := unreflectResult(returns) if err != nil { - WriteRPCResponseHTTP(w, types.NewRPCResponse(request.ID, result, fmt.Sprintf("Error unreflecting result: %v", err.Error()))) + WriteRPCResponseHTTP(w, types.NewRPCResponse(request.ID, result, err.Error())) return } WriteRPCResponseHTTP(w, types.NewRPCResponse(request.ID, result, "")) @@ -231,7 +231,7 @@ func makeHTTPHandler(rpcFunc *RPCFunc) func(http.ResponseWriter, *http.Request) log.Info("HTTPRestRPC", "method", r.URL.Path, "args", args, "returns", returns) result, err := unreflectResult(returns) if err != nil { - WriteRPCResponseHTTP(w, types.NewRPCResponse("", nil, fmt.Sprintf("Error unreflecting result: %v", err.Error()))) + WriteRPCResponseHTTP(w, types.NewRPCResponse("", nil, err.Error())) return } WriteRPCResponseHTTP(w, types.NewRPCResponse("", result, "")) From 4b30cb3083f8275a8d9c94527c290996a42c3707 Mon Sep 17 00:00:00 2001 From: Ethan Buchman Date: Wed, 12 Apr 2017 19:30:05 -0400 Subject: [PATCH 35/38] test: check err on cmd.Wait --- rpc_test.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/rpc_test.go b/rpc_test.go index 8a05d7295..56b8ade32 100644 --- a/rpc_test.go +++ b/rpc_test.go @@ -71,7 +71,9 @@ func init() { if err != nil { panic(err) } - err = cmd.Wait() + if err = cmd.Wait(); err != nil { + panic(err) + } mux := http.NewServeMux() server.RegisterRPCFuncs(mux, Routes) From 1a42f946dc6bcd88f9f58c7f2fb86f785584d793 Mon Sep 17 00:00:00 2001 From: Ethan Buchman Date: Wed, 19 Apr 2017 00:05:18 -0400 Subject: [PATCH 36/38] version bump --- version.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.go b/version.go index 33eb7fe51..8828f260b 100644 --- a/version.go +++ b/version.go @@ -1,7 +1,7 @@ package rpc const Maj = "0" -const Min = "6" // 0x-prefixed string args handled as hex -const Fix = "0" // +const Min = "7" +const Fix = "0" const Version = Maj + "." + Min + "." + Fix From d6fd0c4ca07c0be6d6503d5231060301a3494ab1 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Fri, 21 Apr 2017 18:30:22 +0300 Subject: [PATCH 37/38] fix backward compatibility for WS --- rpc_test.go | 28 ++++++++++++++++++++++++++++ server/handlers.go | 2 +- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/rpc_test.go b/rpc_test.go index 56b8ade32..ed28cbc8d 100644 --- a/rpc_test.go +++ b/rpc_test.go @@ -261,6 +261,34 @@ func TestWSNewWSRPCFunc(t *testing.T) { } } +func TestWSHandlesArrayParams(t *testing.T) { + cl := client.NewWSClient(tcpAddr, websocketEndpoint) + _, err := cl.Start() + require.Nil(t, err) + defer cl.Stop() + + val := "acbd" + params := []interface{}{val} + err = cl.WriteJSON(types.RPCRequest{ + JSONRPC: "2.0", + ID: "", + Method: "echo_ws", + Params: params, + }) + require.Nil(t, err) + + select { + case msg := <-cl.ResultsCh: + result := new(Result) + wire.ReadJSONPtr(result, msg, &err) + require.Nil(t, err) + got := (*result).(*ResultEcho).Value + assert.Equal(t, got, val) + case err := <-cl.ErrorsCh: + t.Fatalf("%+v", err) + } +} + func randBytes(t *testing.T) []byte { n := rand.Intn(10) + 2 buf := make([]byte, n) diff --git a/server/handlers.go b/server/handlers.go index de2b8094f..9fa327cbb 100644 --- a/server/handlers.go +++ b/server/handlers.go @@ -173,7 +173,7 @@ func jsonParamsToArgs(rpcFunc *RPCFunc, paramsI interface{}, argsOffset int) ([] } values := make([]reflect.Value, len(params)) for i, p := range params { - ty := rpcFunc.args[i] + ty := rpcFunc.args[i+argsOffset] v, err := _jsonObjectToArg(ty, p) if err != nil { return nil, err From a01cff9ce630a93168ff7aa003072a7164bdc1ef Mon Sep 17 00:00:00 2001 From: Ethan Buchman Date: Fri, 21 Apr 2017 12:18:21 -0400 Subject: [PATCH 38/38] jsonParamsToArgsRPC func --- server/handlers.go | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/server/handlers.go b/server/handlers.go index 9fa327cbb..9be64b775 100644 --- a/server/handlers.go +++ b/server/handlers.go @@ -124,7 +124,7 @@ func makeJSONRPCHandler(funcMap map[string]*RPCFunc) http.HandlerFunc { WriteRPCResponseHTTP(w, types.NewRPCResponse(request.ID, nil, "RPC method is only for websockets: "+request.Method)) return } - args, err := jsonParamsToArgs(rpcFunc, request.Params, 0) + args, err := jsonParamsToArgsRPC(rpcFunc, request.Params) if err != nil { WriteRPCResponseHTTP(w, types.NewRPCResponse(request.ID, nil, fmt.Sprintf("Error converting json params to arguments: %v", err.Error()))) return @@ -142,7 +142,7 @@ func makeJSONRPCHandler(funcMap map[string]*RPCFunc) http.HandlerFunc { // Convert a []interface{} OR a map[string]interface{} to properly typed values // -// argsOffset is used in jsonParamsToArgsWS, where len(rpcFunc.args) != len(rpcFunc.argNames). +// argsOffset should be 0 for RPC calls, and 1 for WS requests, where len(rpcFunc.args) != len(rpcFunc.argNames). // Example: // rpcFunc.args = [rpctypes.WSRPCContext string] // rpcFunc.argNames = ["arg"] @@ -187,6 +187,11 @@ func jsonParamsToArgs(rpcFunc *RPCFunc, paramsI interface{}, argsOffset int) ([] return values, nil } +// Convert a []interface{} OR a map[string]interface{} to properly typed values +func jsonParamsToArgsRPC(rpcFunc *RPCFunc, paramsI interface{}) ([]reflect.Value, error) { + return jsonParamsToArgs(rpcFunc, paramsI, 0) +} + // Same as above, but with the first param the websocket connection func jsonParamsToArgsWS(rpcFunc *RPCFunc, paramsI interface{}, wsCtx types.WSRPCContext) ([]reflect.Value, error) { values, err := jsonParamsToArgs(rpcFunc, paramsI, 1) @@ -494,7 +499,7 @@ func (wsc *wsConnection) readRoutine() { wsCtx := types.WSRPCContext{Request: request, WSRPCConnection: wsc} args, err = jsonParamsToArgsWS(rpcFunc, request.Params, wsCtx) } else { - args, err = jsonParamsToArgs(rpcFunc, request.Params, 0) + args, err = jsonParamsToArgsRPC(rpcFunc, request.Params) } if err != nil { wsc.WriteRPCResponse(types.NewRPCResponse(request.ID, nil, err.Error()))