Browse Source

Merge pull request #9 from tendermint/feature/8-http-interface-and-1-key-value-params-json-rpc

support key-value params in JSONRPC
pull/456/head
Anton Kaliaev 8 years ago
committed by GitHub
parent
commit
759060f47e
16 changed files with 446 additions and 262 deletions
  1. +15
    -0
      .editorconfig
  2. +12
    -0
      Dockerfile
  3. +11
    -5
      Makefile
  4. +25
    -10
      README.md
  5. +1
    -3
      circle.yml
  6. +29
    -27
      client/http_client.go
  7. +11
    -11
      client/ws_client.go
  8. +139
    -37
      rpc_test.go
  9. +74
    -69
      server/handlers.go
  10. +8
    -7
      server/http_params.go
  11. +9
    -10
      server/http_server.go
  12. +7
    -4
      test/data.json
  13. +95
    -0
      test/integration_test.sh
  14. +3
    -3
      test/main.go
  15. +0
    -69
      test/test.sh
  16. +7
    -7
      types/types.go

+ 15
- 0
.editorconfig View File

@ -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

+ 12
- 0
Dockerfile View File

@ -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

+ 11
- 5
Makefile View File

@ -1,9 +1,15 @@
.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)
.PHONY: all test get_deps

+ 25
- 10
README.md View File

@ -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,25 @@ 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)
## 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)

+ 1
- 3
circle.yml View File

@ -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:


+ 29
- 27
client/http_client.go View File

@ -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 ClientJSONRPC and ClientURI.
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,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,19 +67,26 @@ func NewClientJSONRPC(remote string) *ClientJSONRPC {
}
}
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 *ClientJSONRPC) 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 {
// 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",
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)
@ -113,10 +119,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
@ -142,19 +144,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
}


+ 11
- 11
client/ws_client.go View File

@ -2,14 +2,14 @@ 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"
)
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
}
@ -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))
@ -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
@ -118,22 +118,22 @@ 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",
Params: []interface{}{eventid},
Params: map[string]interface{}{"event": eventid},
})
return err
}
// 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",
Params: []interface{}{eventid},
Params: map[string]interface{}{"event": eventid},
})
return err
}

+ 139
- 37
rpc_test.go View File

@ -1,20 +1,26 @@
package rpc
import (
"bytes"
crand "crypto/rand"
"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"
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"
)
@ -26,14 +32,21 @@ 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]*rpcserver.RPCFunc{
"status": rpcserver.NewRPCFunc(StatusResult, "arg"),
var Routes = map[string]*server.RPCFunc{
"status": server.NewRPCFunc(StatusResult, "arg"),
"status_ws": server.NewWSRPCFunc(StatusWSResult, "arg"),
"bytes": server.NewRPCFunc(BytesResult, "arg"),
}
// an rpc function
@ -41,25 +54,40 @@ 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
}
// 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()
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 +98,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,9 +114,11 @@ 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 := []interface{}{val}
params := map[string]interface{}{
"arg": val,
}
var result Result
_, err := cl.Call("status", params, &result)
if err != nil {
@ -100,10 +130,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 := []interface{}{val}
err := cl.WriteJSON(rpctypes.RPCRequest{
params := map[string]interface{}{
"arg": val,
}
err := cl.WriteJSON(types.RPCRequest{
JSONRPC: "2.0",
ID: "",
Method: "status",
@ -113,42 +145,46 @@ func testWS(t *testing.T, cl *rpcclient.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)
}
}
//-------------
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)
@ -157,7 +193,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)
@ -166,7 +202,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{}{
@ -184,7 +220,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{}{
@ -200,3 +236,69 @@ 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)
}
}
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)
}
}

+ 74
- 69
server/handlers.go View File

@ -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,75 @@ 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 := jsonParamsToArgs(rpcFunc, request.Params, 0)
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) {
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
//
// 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+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
}
return values, nil
}
// 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))
}
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
func jsonParamsToArgsWS(rpcFunc *RPCFunc, params map[string]interface{}, wsCtx types.WSRPCContext) ([]reflect.Value, error) {
values, err := jsonParamsToArgs(rpcFunc, params, 1)
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) {
@ -197,7 +196,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 +204,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, fmt.Sprintf("Error unreflecting result: %v", 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 +245,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 +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
}
@ -313,11 +318,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 +335,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 +404,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 +415,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 +449,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 +461,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 = jsonParamsToArgs(rpcFunc, request.Params, 0)
}
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 +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,


+ 8
- 7
server/http_params.go View File

@ -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
}

+ 9
- 10
server/http_server.go View File

@ -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)))
}
}


+ 7
- 4
test/data.json View File

@ -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
}
}

+ 95
- 0
test/integration_test.sh View File

@ -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

+ 3
- 3
test/main.go View File

@ -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() {
})
}

+ 0
- 69
test/test.sh View File

@ -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

+ 7
- 7
types/types.go View File

@ -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,


Loading…
Cancel
Save