diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..d587999e --- /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/Dockerfile b/Dockerfile new file mode 100644 index 00000000..c23c911c --- /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 diff --git a/Makefile b/Makefile index 3b005ea3..0937558a 100644 --- a/Makefile +++ b/Makefile @@ -1,9 +1,18 @@ -.PHONY: all test get_deps +PACKAGES=$(shell go list ./... | grep -v "test") -all: test +all: get_deps 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) + @go list -f '{{join .TestImports "\n"}}' ./... | \ + grep -v /vendor/ | sort | uniq | \ + xargs go get -v -d + +.PHONY: all test get_deps diff --git a/README.md b/README.md index 1a91fb69..79dd9692 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,10 +102,27 @@ 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 * [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 + +### 0.7.0 + +BREAKING CHANGES: + +- removed `Client` empty interface +- `ClientJSONRPC#Call` `params` argument became a map +- rename `ClientURI` -> `URIClient`, `ClientJSONRPC` -> `JSONRPCClient` + +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/circle.yml b/circle.yml index 99af678c..0308a4e7 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: diff --git a/client/http_client.go b/client/http_client.go index 57da5d6e..f4a2a6d7 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" + "github.com/pkg/errors" + types "github.com/tendermint/go-rpc/types" + wire "github.com/tendermint/go-wire" ) +// HTTPClient is a common interface for JSONRPCClient and URIClient. +type HTTPClient interface { + Call(method string, params map[string]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)) { @@ -24,7 +28,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] @@ -49,38 +53,39 @@ func makeHTTPClient(remoteAddr string) (string, *http.Client) { //------------------------------------------------------------------------------------ -type Client interface { -} - -//------------------------------------------------------------------------------------ - // 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 []interface{}, result interface{}) (interface{}, error) { - return c.call(method, params, result) -} - -func (c *ClientJSONRPC) call(method string, params []interface{}, result interface{}) (interface{}, error) { - // Make request and get responseBytes - request := rpctypes.RPCRequest{ +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{}) + for k, v := range params { + bytes := json.RawMessage(wire.JSONBytes(v)) + encodedParams[k] = &bytes + } + request := types.RPCRequest{ JSONRPC: "2.0", Method: method, - Params: params, + Params: encodedParams, ID: "", } - requestBytes := wire.JSONBytes(request) + requestBytes, err := json.Marshal(request) + 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) @@ -99,24 +104,20 @@ func (c *ClientJSONRPC) call(method string, params []interface{}, result interfa //------------------------------------------------------------- // 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) { - return c.call(method, params, result) -} - -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 @@ -142,19 +143,19 @@ 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, errors.New(Fmt("Error unmarshalling rpc response: %v", err)) + return nil, errors.Errorf("Error unmarshalling rpc response: %v", err) } errorStr := response.Error if errorStr != "" { - return nil, errors.New(Fmt("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, errors.New(Fmt("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 4d975f8e..16dc474c 100644 --- a/client/ws_client.go +++ b/client/ws_client.go @@ -2,14 +2,15 @@ package rpcclient import ( "encoding/json" - "fmt" "net" "net/http" "time" "github.com/gorilla/websocket" - . "github.com/tendermint/go-common" - "github.com/tendermint/go-rpc/types" + "github.com/pkg/errors" + cmn "github.com/tendermint/go-common" + types "github.com/tendermint/go-rpc/types" + wire "github.com/tendermint/go-wire" ) const ( @@ -19,7 +20,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) @@ -32,14 +33,12 @@ 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 = *NewBaseService(log, "WSClient", wsClient) + wsClient.BaseService = *cmn.NewBaseService(log, "WSClient", wsClient) return wsClient } @@ -47,16 +46,24 @@ 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 } + 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 { + return nil +} + func (wsc *WSClient) dial() error { // Dial @@ -83,8 +90,10 @@ func (wsc *WSClient) dial() error { return nil } +// OnStop implements cmn.BaseService interface func (wsc *WSClient) OnStop() { wsc.BaseService.OnStop() + wsc.Conn.Close() // ResultsCh/ErrorsCh is closed in receiveEventsRoutine. } @@ -96,7 +105,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)) @@ -104,36 +113,60 @@ 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 } } + // 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) 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(rpctypes.RPCRequest{ + err := wsc.WriteJSON(types.RPCRequest{ JSONRPC: "2.0", ID: "", Method: "subscribe", - Params: []interface{}{eventid}, + Params: map[string]interface{}{"event": eventid}, }) 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(rpctypes.RPCRequest{ + err := wsc.WriteJSON(types.RPCRequest{ JSONRPC: "2.0", ID: "", Method: "unsubscribe", - Params: []interface{}{eventid}, + Params: map[string]interface{}{"event": eventid}, + }) + 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 1fcda0e5..ed28cbc8 100644 --- a/rpc_test.go +++ b/rpc_test.go @@ -1,20 +1,29 @@ package rpc import ( + "bytes" + crand "crypto/rand" + "fmt" + "math/rand" "net/http" + "os/exec" "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" + "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" + wire "github.com/tendermint/go-wire" ) // Client and Server should work over tcp or unix sockets -var ( - tcpAddr = "tcp://0.0.0.0:46657" - unixAddr = "unix:///tmp/go-rpc.sock" // NOTE: must remove file for test to run again +const ( + tcpAddr = "tcp://0.0.0.0:46657" + + unixSocket = "/tmp/go-rpc.sock" + unixAddr = "unix:///tmp/go-rpc.sock" websocketEndpoint = "/websocket/endpoint" ) @@ -22,44 +31,67 @@ var ( // Define a type for results and register concrete versions type Result interface{} -type ResultStatus struct { +type ResultEcho struct { Value string } +type ResultEchoBytes struct { + Value []byte +} + var _ = wire.RegisterInterface( struct{ Result }{}, - wire.ConcreteType{&ResultStatus{}, 0x1}, + wire.ConcreteType{&ResultEcho{}, 0x1}, + wire.ConcreteType{&ResultEchoBytes{}, 0x2}, ) // Define some routes -var Routes = map[string]*rpcserver.RPCFunc{ - "status": rpcserver.NewRPCFunc(StatusResult, "arg"), +var Routes = map[string]*server.RPCFunc{ + "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 EchoWSResult(wsCtx types.WSRPCContext, v string) (Result, error) { + return &ResultEcho{v}, nil +} + +func EchoBytesResult(v []byte) (Result, error) { + return &ResultEchoBytes{v}, nil } // launch unix and tcp servers func init() { + cmd := exec.Command("rm", "-f", unixSocket) + err := cmd.Start() + if err != nil { + panic(err) + } + if err = cmd.Wait(); err != nil { + panic(err) + } + 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) } @@ -67,136 +99,200 @@ func init() { // wait for servers to start time.Sleep(time.Second * 2) - } -func testURI(t *testing.T, cl *rpcclient.ClientURI) { - val := "acbd" +func echoViaHTTP(cl client.HTTPClient, val string) (string, error) { params := map[string]interface{}{ "arg": val, } var result Result - _, err := cl.Call("status", params, &result) - if err != nil { - t.Fatal(err) - } - got := result.(*ResultStatus).Value - if got != val { - t.Fatalf("Got: %v .... Expected: %v \n", got, val) + if _, err := cl.Call("echo", params, &result); err != nil { + return "", err } + return result.(*ResultEcho).Value, nil } -func testJSONRPC(t *testing.T, cl *rpcclient.ClientJSONRPC) { - val := "acbd" - params := []interface{}{val} +func echoBytesViaHTTP(cl client.HTTPClient, bytes []byte) ([]byte, error) { + params := map[string]interface{}{ + "arg": bytes, + } var result Result - _, err := cl.Call("status", params, &result) - if err != nil { - t.Fatal(err) + if _, err := cl.Call("echo_bytes", params, &result); err != nil { + return []byte{}, err } - got := result.(*ResultStatus).Value - if got != val { - t.Fatalf("Got: %v .... Expected: %v \n", got, val) + return result.(*ResultEchoBytes).Value, nil +} + +func testWithHTTPClient(t *testing.T, cl client.HTTPClient) { + val := "acbd" + 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 echoViaWS(cl *client.WSClient, val string) (string, error) { + params := map[string]interface{}{ + "arg": val, + } + err := cl.Call("echo", params) + if err != nil { + return "", err + } + + select { + case msg := <-cl.ResultsCh: + result := new(Result) + wire.ReadJSONPtr(result, msg, &err) + if err != nil { + return "", nil + } + return (*result).(*ResultEcho).Value, nil + case err := <-cl.ErrorsCh: + return "", err } } -func testWS(t *testing.T, cl *rpcclient.WSClient) { - val := "acbd" - params := []interface{}{val} - err := cl.WriteJSON(rpctypes.RPCRequest{ - JSONRPC: "2.0", - ID: "", - Method: "status", - Params: params, - }) +func echoBytesViaWS(cl *client.WSClient, bytes []byte) ([]byte, error) { + params := map[string]interface{}{ + "arg": bytes, + } + err := cl.Call("echo_bytes", params) if err != nil { - t.Fatal(err) + return []byte{}, err } - 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) + 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 TestURI_TCP(t *testing.T) { - cl := rpcclient.NewClientURI(tcpAddr) - testURI(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 TestURI_UNIX(t *testing.T) { - cl := rpcclient.NewClientURI(unixAddr) - testURI(t, cl) -} + cl2 := client.NewJSONRPCClient(tcpAddr) + fmt.Printf("=== testing server on %s using %v client", addr, cl2) + testWithHTTPClient(t, cl2) -func TestJSONRPC_TCP(t *testing.T) { - cl := rpcclient.NewClientJSONRPC(tcpAddr) - testJSONRPC(t, cl) -} - -func TestJSONRPC_UNIX(t *testing.T) { - cl := rpcclient.NewClientJSONRPC(unixAddr) - testJSONRPC(t, cl) -} - -func TestWS_TCP(t *testing.T) { - cl := rpcclient.NewWSClient(tcpAddr, websocketEndpoint) - _, err := cl.Start() - if err != nil { - t.Fatal(err) + 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() } - testWS(t, cl) -} - -func TestWS_UNIX(t *testing.T) { - cl := rpcclient.NewWSClient(unixAddr, websocketEndpoint) - _, err := cl.Start() - if err != nil { - t.Fatal(err) - } - testWS(t, cl) } func TestHexStringArg(t *testing.T) { - cl := rpcclient.NewClientURI(tcpAddr) + 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) - if err != nil { - t.Fatal(err) - } - got := result.(*ResultStatus).Value - if got != val { - t.Fatalf("Got: %v .... Expected: %v \n", got, val) - } + got, err := echoViaHTTP(cl, val) + require.Nil(t, err) + assert.Equal(t, got, val) } func TestQuotedStringArg(t *testing.T) { - cl := rpcclient.NewClientURI(tcpAddr) + cl := client.NewURIClient(tcpAddr) // should NOT be unquoted val := "\"abc\"" + got, err := echoViaHTTP(cl, val) + require.Nil(t, err) + assert.Equal(t, got, val) +} + +func TestWSNewWSRPCFunc(t *testing.T) { + cl := client.NewWSClient(tcpAddr, websocketEndpoint) + _, err := cl.Start() + require.Nil(t, err) + defer cl.Stop() + + val := "acbd" params := map[string]interface{}{ "arg": val, } - var result Result - _, err := cl.Call("status", params, &result) - if err != nil { + 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.Fatal(err) } - got := result.(*ResultStatus).Value - if got != val { - t.Fatalf("Got: %v .... Expected: %v \n", got, val) +} + +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) + _, err := crand.Read(buf) + require.Nil(t, err) + return bytes.Replace(buf, []byte("="), []byte{100}, -1) +} diff --git a/server/handlers.go b/server/handlers.go index 7a4ec1a4..9be64b77 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,11 @@ 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" + "github.com/pkg/errors" + 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,76 +105,100 @@ 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) + args, err := jsonParamsToArgsRPC(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, 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) { - 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 +// Convert a []interface{} OR a map[string]interface{} to properly typed values +// +// 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"] +func jsonParamsToArgs(rpcFunc *RPCFunc, paramsI interface{}, argsOffset int) ([]reflect.Value, error) { + values := make([]reflect.Value, len(rpcFunc.argNames)) + + switch params := paramsI.(type) { + + 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) + } } - values[i] = v + 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+argsOffset] + v, err := _jsonObjectToArg(ty, p) + if err != nil { + return nil, err + } + values[i] = v + } + 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 } +// 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, params []interface{}, wsCtx 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)) +func jsonParamsToArgsWS(rpcFunc *RPCFunc, paramsI interface{}, wsCtx types.WSRPCContext) ([]reflect.Value, error) { + values, err := jsonParamsToArgs(rpcFunc, paramsI, 1) + if err != nil { + return nil, err } - values := make([]reflect.Value, len(params)+1) - values[0] = reflect.ValueOf(wsCtx) - for i, p := range params { - ty := rpcFunc.args[i+1] - v, err := _jsonObjectToArg(ty, p) - if err != nil { - return nil, err - } - values[i+1] = v - } - return values, nil + return append([]reflect.Value{reflect.ValueOf(wsCtx)}, values...), nil } func _jsonObjectToArg(ty reflect.Type, object interface{}) (reflect.Value, error) { @@ -197,7 +221,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,33 +229,38 @@ 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, err.Error())) return } - WriteRPCResponseHTTP(w, NewRPCResponse("", result, "")) + WriteRPCResponseHTTP(w, types.NewRPCResponse("", result, "")) } } // 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)) + + for i, name := range rpcFunc.argNames { + argType := rpcFunc.args[i] + + values[i] = reflect.Zero(argType) // set default for that type - values := make([]reflect.Value, len(argNames)) - for i, name := range argNames { - ty := argTypes[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) - v, err, ok := nonJsonToArg(ty, arg) + if "" == arg { + continue + } + + v, err, ok := nonJsonToArg(argType, arg) if err != nil { return nil, err } @@ -241,11 +270,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 } @@ -268,7 +298,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 } @@ -313,11 +343,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 +360,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 } @@ -342,12 +372,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)) @@ -368,7 +401,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 @@ -399,7 +434,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 +445,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 +479,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 +491,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) + args, err = jsonParamsToArgsRPC(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 } @@ -563,7 +598,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 acf5b4c8..56506067 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 26163cf1..5375c574 100644 --- a/server/http_server.go +++ b/server/http_server.go @@ -11,9 +11,8 @@ import ( "strings" "time" - . "github.com/tendermint/go-common" - . "github.com/tendermint/go-rpc/types" - //"github.com/tendermint/go-wire" + "github.com/pkg/errors" + types "github.com/tendermint/go-rpc/types" ) func StartHTTPServer(listenAddr string, handler http.Handler) (listener net.Listener, err error) { @@ -24,17 +23,17 @@ 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) + // return nil, errors.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) + return nil, errors.Errorf("Failed to listen to %v: %v", listenAddr, err) } go func() { @@ -47,7 +46,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 +82,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))) } } diff --git a/test/data.json b/test/data.json index eac2e0df..83283ec3 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/integration_test.sh b/test/integration_test.sh new file mode 100755 index 00000000..7c23be7d --- /dev/null +++ b/test/integration_test.sh @@ -0,0 +1,95 @@ +#!/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 )" + +# Change into that dir because we expect that. +pushd "$DIR" + +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 + +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 + echo "responses are not identical:" + echo "R1: $R1" + echo "R2: $R2" + echo "FAIL" + exit 1 +else + echo "OK" +fi + +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 + echo "responses are not identical:" + echo "R1: $R1" + echo "R2: $R2" + echo "FAIL" + exit 1 +else + echo "OK" +fi + +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" +fi + +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 + echo "responses are not identical:" + echo "R1: $R1" + echo "R2: $R2" + echo "FAIL" + exit 1 +else + echo "OK" +fi + +echo "==> Stopping the server" +kill -9 $PID + +rm -f rpcserver + +popd +exit 0 diff --git a/test/main.go b/test/main.go index d14ab05f..28de2be8 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/test/test.sh b/test/test.sh deleted file mode 100755 index f5e74024..00000000 --- a/test/test.sh +++ /dev/null @@ -1,69 +0,0 @@ -#! /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/... - - -# integration tests -cd test -set -e - -go build -o server main.go -./server > /dev/null & -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` -if [[ "$R1" != "$R2" ]]; then - echo "responses are not identical:" - echo "R1: $R1" - echo "R2: $R2" - exit 1 -else - echo "Success" -fi - -# 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 - echo "responses are not identical:" - echo "R1: $R1" - echo "R2: $R2" - exit 1 -else - echo "Success" -fi - -# 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" - exit 1 -else - echo "Success" -fi - -# 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 - echo "responses are not identical:" - echo "R1: $R1" - echo "R2: $R2" - exit 1 -else - echo "Success" -fi - -kill -9 $PID || exit 0 diff --git a/types/types.go b/types/types.go index ee4a63cc..38c7f09d 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 interface{} `json:"params"` // must be map[string]interface{} or []interface{} } -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, diff --git a/version.go b/version.go index 33eb7fe5..8828f260 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