Merge pull request #17 from tendermint/develop

v0.7.0
This commit is contained in:
Ethan Buchman 2017-04-21 13:08:14 -04:00 committed by GitHub
commit 2c8df0ee6b
17 changed files with 603 additions and 358 deletions

15
.editorconfig Normal file
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
Dockerfile Normal file
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

View File

@ -1,9 +1,18 @@
.PHONY: all test get_deps PACKAGES=$(shell go list ./... | grep -v "test")
all: test all: get_deps test
test: test:
bash ./test/test.sh @echo "--> Running go test --race"
@go test --race $(PACKAGES)
@echo "--> Running integration tests"
@bash ./test/integration_test.sh
get_deps: 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

View File

@ -32,16 +32,16 @@ As a POST request, we use JSONRPC. For instance, the same request would have thi
``` ```
{ {
"jsonrpc":"2.0", "jsonrpc": "2.0",
"id":"anything", "id": "anything",
"method":"hello_world", "method": "hello_world",
"params":["my_world", 5] "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 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) ## WebSocket (JSONRPC)
All requests are exposed over websocket in the same form as the POST 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`, Websocket connections are available at their own endpoint, typically `/websocket`,
though this is configurable when starting the server. though this is configurable when starting the server.
# Server Definition # 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`) 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`. 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 # Examples
* [Tendermint](https://github.com/tendermint/tendermint/blob/master/rpc/core/routes.go) * [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)

View File

@ -11,12 +11,10 @@ checkout:
- rm -rf $REPO - rm -rf $REPO
- mkdir -p $HOME/.go_workspace/src/github.com/$CIRCLE_PROJECT_USERNAME - mkdir -p $HOME/.go_workspace/src/github.com/$CIRCLE_PROJECT_USERNAME
- mv $HOME/$CIRCLE_PROJECT_REPONAME $REPO - mv $HOME/$CIRCLE_PROJECT_REPONAME $REPO
# - git submodule sync
# - git submodule update --init # use submodules
dependencies: dependencies:
override: override:
- "cd $REPO" - "cd $REPO && make get_deps"
test: test:
override: override:

View File

@ -3,7 +3,6 @@ package rpcclient
import ( import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"net" "net"
@ -12,11 +11,16 @@ import (
"reflect" "reflect"
"strings" "strings"
. "github.com/tendermint/go-common" "github.com/pkg/errors"
"github.com/tendermint/go-rpc/types" types "github.com/tendermint/go-rpc/types"
"github.com/tendermint/go-wire" 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 // TODO: Deprecate support for IP:PORT or /path/to/socket
func makeHTTPDialer(remoteAddr string) (string, func(string, string) (net.Conn, error)) { 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 var protocol, address string
if len(parts) != 2 { if len(parts) != 2 {
log.Warn("WARNING (go-rpc): Please use fully formed listening addresses, including the tcp:// or unix:// prefix") 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 address = remoteAddr
} else { } else {
protocol, address = parts[0], parts[1] 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 // JSON rpc takes params as a slice
type ClientJSONRPC struct { type JSONRPCClient struct {
address string address string
client *http.Client client *http.Client
} }
func NewClientJSONRPC(remote string) *ClientJSONRPC { func NewJSONRPCClient(remote string) *JSONRPCClient {
address, client := makeHTTPClient(remote) address, client := makeHTTPClient(remote)
return &ClientJSONRPC{ return &JSONRPCClient{
address: address, address: address,
client: client, client: client,
} }
} }
func (c *ClientJSONRPC) Call(method string, params []interface{}, result interface{}) (interface{}, error) { func (c *JSONRPCClient) Call(method string, params map[string]interface{}, result interface{}) (interface{}, error) {
return c.call(method, params, result) // 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{})
func (c *ClientJSONRPC) call(method string, params []interface{}, result interface{}) (interface{}, error) { for k, v := range params {
// Make request and get responseBytes bytes := json.RawMessage(wire.JSONBytes(v))
request := rpctypes.RPCRequest{ encodedParams[k] = &bytes
}
request := types.RPCRequest{
JSONRPC: "2.0", JSONRPC: "2.0",
Method: method, Method: method,
Params: params, Params: encodedParams,
ID: "", ID: "",
} }
requestBytes := wire.JSONBytes(request) requestBytes, err := json.Marshal(request)
if err != nil {
return nil, err
}
// log.Info(string(requestBytes))
requestBuf := bytes.NewBuffer(requestBytes) requestBuf := bytes.NewBuffer(requestBytes)
// log.Info(Fmt("RPC request to %v (%v): %v", c.remote, method, string(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) 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 // URI takes params as a map
type ClientURI struct { type URIClient struct {
address string address string
client *http.Client client *http.Client
} }
func NewClientURI(remote string) *ClientURI { func NewURIClient(remote string) *URIClient {
address, client := makeHTTPClient(remote) address, client := makeHTTPClient(remote)
return &ClientURI{ return &URIClient{
address: address, address: address,
client: client, 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) {
return c.call(method, params, result)
}
func (c *ClientURI) call(method string, params map[string]interface{}, result interface{}) (interface{}, error) {
values, err := argsToURLValues(params) values, err := argsToURLValues(params)
if err != nil { if err != nil {
return nil, err return nil, err
@ -142,19 +143,19 @@ func unmarshalResponseBytes(responseBytes []byte, result interface{}) (interface
// into the correct type // into the correct type
// log.Notice("response", "response", string(responseBytes)) // log.Notice("response", "response", string(responseBytes))
var err error var err error
response := &rpctypes.RPCResponse{} response := &types.RPCResponse{}
err = json.Unmarshal(responseBytes, response) err = json.Unmarshal(responseBytes, response)
if err != nil { 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 errorStr := response.Error
if errorStr != "" { 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 // unmarshal the RawMessage into the result
result = wire.ReadJSONPtr(result, *response.Result, &err) result = wire.ReadJSONPtr(result, *response.Result, &err)
if err != nil { 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 return result, nil
} }

View File

@ -2,14 +2,15 @@ package rpcclient
import ( import (
"encoding/json" "encoding/json"
"fmt"
"net" "net"
"net/http" "net/http"
"time" "time"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
. "github.com/tendermint/go-common" "github.com/pkg/errors"
"github.com/tendermint/go-rpc/types" cmn "github.com/tendermint/go-common"
types "github.com/tendermint/go-rpc/types"
wire "github.com/tendermint/go-wire"
) )
const ( const (
@ -19,7 +20,7 @@ const (
) )
type WSClient struct { type WSClient struct {
BaseService cmn.BaseService
Address string // IP:PORT or /path/to/socket Address string // IP:PORT or /path/to/socket
Endpoint string // /websocket/url/endpoint Endpoint string // /websocket/url/endpoint
Dialer func(string, string) (net.Conn, error) Dialer func(string, string) (net.Conn, error)
@ -32,14 +33,12 @@ type WSClient struct {
func NewWSClient(remoteAddr, endpoint string) *WSClient { func NewWSClient(remoteAddr, endpoint string) *WSClient {
addr, dialer := makeHTTPDialer(remoteAddr) addr, dialer := makeHTTPDialer(remoteAddr)
wsClient := &WSClient{ wsClient := &WSClient{
Address: addr, Address: addr,
Dialer: dialer, Dialer: dialer,
Endpoint: endpoint, Endpoint: endpoint,
Conn: nil, Conn: nil,
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 return wsClient
} }
@ -47,16 +46,24 @@ func (wsc *WSClient) String() string {
return wsc.Address + ", " + wsc.Endpoint return wsc.Address + ", " + wsc.Endpoint
} }
// OnStart implements cmn.BaseService interface
func (wsc *WSClient) OnStart() error { func (wsc *WSClient) OnStart() error {
wsc.BaseService.OnStart() wsc.BaseService.OnStart()
err := wsc.dial() err := wsc.dial()
if err != nil { if err != nil {
return err return err
} }
wsc.ResultsCh = make(chan json.RawMessage, wsResultsChannelCapacity)
wsc.ErrorsCh = make(chan error, wsErrorsChannelCapacity)
go wsc.receiveEventsRoutine() go wsc.receiveEventsRoutine()
return nil return nil
} }
// OnReset implements cmn.BaseService interface
func (wsc *WSClient) OnReset() error {
return nil
}
func (wsc *WSClient) dial() error { func (wsc *WSClient) dial() error {
// Dial // Dial
@ -83,8 +90,10 @@ func (wsc *WSClient) dial() error {
return nil return nil
} }
// OnStop implements cmn.BaseService interface
func (wsc *WSClient) OnStop() { func (wsc *WSClient) OnStop() {
wsc.BaseService.OnStop() wsc.BaseService.OnStop()
wsc.Conn.Close()
// ResultsCh/ErrorsCh is closed in receiveEventsRoutine. // ResultsCh/ErrorsCh is closed in receiveEventsRoutine.
} }
@ -96,7 +105,7 @@ func (wsc *WSClient) receiveEventsRoutine() {
wsc.Stop() wsc.Stop()
break break
} else { } else {
var response rpctypes.RPCResponse var response types.RPCResponse
err := json.Unmarshal(data, &response) err := json.Unmarshal(data, &response)
if err != nil { if err != nil {
log.Info("WSClient failed to parse message", "error", err, "data", string(data)) log.Info("WSClient failed to parse message", "error", err, "data", string(data))
@ -104,36 +113,60 @@ func (wsc *WSClient) receiveEventsRoutine() {
continue continue
} }
if response.Error != "" { if response.Error != "" {
wsc.ErrorsCh <- fmt.Errorf(response.Error) wsc.ErrorsCh <- errors.Errorf(response.Error)
continue continue
} }
wsc.ResultsCh <- *response.Result 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 // Cleanup
close(wsc.ResultsCh) close(wsc.ResultsCh)
close(wsc.ErrorsCh) 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 { func (wsc *WSClient) Subscribe(eventid string) error {
err := wsc.WriteJSON(rpctypes.RPCRequest{ err := wsc.WriteJSON(types.RPCRequest{
JSONRPC: "2.0", JSONRPC: "2.0",
ID: "", ID: "",
Method: "subscribe", Method: "subscribe",
Params: []interface{}{eventid}, Params: map[string]interface{}{"event": eventid},
}) })
return err 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 { func (wsc *WSClient) Unsubscribe(eventid string) error {
err := wsc.WriteJSON(rpctypes.RPCRequest{ err := wsc.WriteJSON(types.RPCRequest{
JSONRPC: "2.0", JSONRPC: "2.0",
ID: "", ID: "",
Method: "unsubscribe", 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 return err
} }

View File

@ -1,20 +1,29 @@
package rpc package rpc
import ( import (
"bytes"
crand "crypto/rand"
"fmt"
"math/rand"
"net/http" "net/http"
"os/exec"
"testing" "testing"
"time" "time"
"github.com/tendermint/go-rpc/client" "github.com/stretchr/testify/assert"
"github.com/tendermint/go-rpc/server" "github.com/stretchr/testify/require"
"github.com/tendermint/go-rpc/types" client "github.com/tendermint/go-rpc/client"
"github.com/tendermint/go-wire" 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 // Client and Server should work over tcp or unix sockets
var ( const (
tcpAddr = "tcp://0.0.0.0:46657" tcpAddr = "tcp://0.0.0.0:46657"
unixAddr = "unix:///tmp/go-rpc.sock" // NOTE: must remove file for test to run again
unixSocket = "/tmp/go-rpc.sock"
unixAddr = "unix:///tmp/go-rpc.sock"
websocketEndpoint = "/websocket/endpoint" websocketEndpoint = "/websocket/endpoint"
) )
@ -22,44 +31,67 @@ var (
// Define a type for results and register concrete versions // Define a type for results and register concrete versions
type Result interface{} type Result interface{}
type ResultStatus struct { type ResultEcho struct {
Value string Value string
} }
type ResultEchoBytes struct {
Value []byte
}
var _ = wire.RegisterInterface( var _ = wire.RegisterInterface(
struct{ Result }{}, struct{ Result }{},
wire.ConcreteType{&ResultStatus{}, 0x1}, wire.ConcreteType{&ResultEcho{}, 0x1},
wire.ConcreteType{&ResultEchoBytes{}, 0x2},
) )
// Define some routes // Define some routes
var Routes = map[string]*rpcserver.RPCFunc{ var Routes = map[string]*server.RPCFunc{
"status": rpcserver.NewRPCFunc(StatusResult, "arg"), "echo": server.NewRPCFunc(EchoResult, "arg"),
"echo_ws": server.NewWSRPCFunc(EchoWSResult, "arg"),
"echo_bytes": server.NewRPCFunc(EchoBytesResult, "arg"),
} }
// an rpc function func EchoResult(v string) (Result, error) {
func StatusResult(v string) (Result, error) { return &ResultEcho{v}, nil
return &ResultStatus{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 // launch unix and tcp servers
func init() { 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() mux := http.NewServeMux()
rpcserver.RegisterRPCFuncs(mux, Routes) server.RegisterRPCFuncs(mux, Routes)
wm := rpcserver.NewWebsocketManager(Routes, nil) wm := server.NewWebsocketManager(Routes, nil)
mux.HandleFunc(websocketEndpoint, wm.WebsocketHandler) mux.HandleFunc(websocketEndpoint, wm.WebsocketHandler)
go func() { go func() {
_, err := rpcserver.StartHTTPServer(tcpAddr, mux) _, err := server.StartHTTPServer(tcpAddr, mux)
if err != nil { if err != nil {
panic(err) panic(err)
} }
}() }()
mux2 := http.NewServeMux() mux2 := http.NewServeMux()
rpcserver.RegisterRPCFuncs(mux2, Routes) server.RegisterRPCFuncs(mux2, Routes)
wm = rpcserver.NewWebsocketManager(Routes, nil) wm = server.NewWebsocketManager(Routes, nil)
mux2.HandleFunc(websocketEndpoint, wm.WebsocketHandler) mux2.HandleFunc(websocketEndpoint, wm.WebsocketHandler)
go func() { go func() {
_, err := rpcserver.StartHTTPServer(unixAddr, mux2) _, err := server.StartHTTPServer(unixAddr, mux2)
if err != nil { if err != nil {
panic(err) panic(err)
} }
@ -67,136 +99,200 @@ func init() {
// wait for servers to start // wait for servers to start
time.Sleep(time.Second * 2) time.Sleep(time.Second * 2)
} }
func testURI(t *testing.T, cl *rpcclient.ClientURI) { func echoViaHTTP(cl client.HTTPClient, val string) (string, error) {
val := "acbd"
params := map[string]interface{}{ params := map[string]interface{}{
"arg": val, "arg": val,
} }
var result Result var result Result
_, err := cl.Call("status", params, &result) if _, err := cl.Call("echo", params, &result); err != nil {
if err != nil { return "", err
t.Fatal(err)
}
got := result.(*ResultStatus).Value
if got != val {
t.Fatalf("Got: %v .... Expected: %v \n", got, val)
} }
return result.(*ResultEcho).Value, nil
} }
func testJSONRPC(t *testing.T, cl *rpcclient.ClientJSONRPC) { func echoBytesViaHTTP(cl client.HTTPClient, bytes []byte) ([]byte, error) {
val := "acbd" params := map[string]interface{}{
params := []interface{}{val} "arg": bytes,
}
var result Result var result Result
_, err := cl.Call("status", params, &result) if _, err := cl.Call("echo_bytes", params, &result); err != nil {
if err != nil { return []byte{}, err
t.Fatal(err)
} }
got := result.(*ResultStatus).Value return result.(*ResultEchoBytes).Value, nil
if got != val { }
t.Fatalf("Got: %v .... Expected: %v \n", got, val)
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) { func echoBytesViaWS(cl *client.WSClient, bytes []byte) ([]byte, error) {
val := "acbd" params := map[string]interface{}{
params := []interface{}{val} "arg": bytes,
err := cl.WriteJSON(rpctypes.RPCRequest{ }
JSONRPC: "2.0", err := cl.Call("echo_bytes", params)
ID: "",
Method: "status",
Params: params,
})
if err != nil { if err != nil {
t.Fatal(err) return []byte{}, err
} }
msg := <-cl.ResultsCh select {
result := new(Result) case msg := <-cl.ResultsCh:
wire.ReadJSONPtr(result, msg, &err) result := new(Result)
if err != nil { wire.ReadJSONPtr(result, msg, &err)
t.Fatal(err) if err != nil {
} return []byte{}, nil
got := (*result).(*ResultStatus).Value }
if got != val { return (*result).(*ResultEchoBytes).Value, nil
t.Fatalf("Got: %v .... Expected: %v \n", got, val) 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) { func TestServersAndClientsBasic(t *testing.T) {
cl := rpcclient.NewClientURI(tcpAddr) serverAddrs := [...]string{tcpAddr, unixAddr}
testURI(t, cl) 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) { cl2 := client.NewJSONRPCClient(tcpAddr)
cl := rpcclient.NewClientURI(unixAddr) fmt.Printf("=== testing server on %s using %v client", addr, cl2)
testURI(t, cl) testWithHTTPClient(t, cl2)
}
func TestJSONRPC_TCP(t *testing.T) { cl3 := client.NewWSClient(tcpAddr, websocketEndpoint)
cl := rpcclient.NewClientJSONRPC(tcpAddr) _, err := cl3.Start()
testJSONRPC(t, cl) require.Nil(t, err)
} fmt.Printf("=== testing server on %s using %v client", addr, cl3)
testWithWSClient(t, cl3)
func TestJSONRPC_UNIX(t *testing.T) { cl3.Stop()
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)
} }
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) { func TestHexStringArg(t *testing.T) {
cl := rpcclient.NewClientURI(tcpAddr) cl := client.NewURIClient(tcpAddr)
// should NOT be handled as hex // should NOT be handled as hex
val := "0xabc" val := "0xabc"
params := map[string]interface{}{ got, err := echoViaHTTP(cl, val)
"arg": val, require.Nil(t, err)
} assert.Equal(t, got, 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)
}
} }
func TestQuotedStringArg(t *testing.T) { func TestQuotedStringArg(t *testing.T) {
cl := rpcclient.NewClientURI(tcpAddr) cl := client.NewURIClient(tcpAddr)
// should NOT be unquoted // should NOT be unquoted
val := "\"abc\"" 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{}{ params := map[string]interface{}{
"arg": val, "arg": val,
} }
var result Result err = cl.WriteJSON(types.RPCRequest{
_, err := cl.Call("status", params, &result) JSONRPC: "2.0",
if err != nil { 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) 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)
}

View File

@ -4,7 +4,6 @@ import (
"bytes" "bytes"
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
@ -14,10 +13,11 @@ import (
"time" "time"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
. "github.com/tendermint/go-common" "github.com/pkg/errors"
"github.com/tendermint/go-events" cmn "github.com/tendermint/go-common"
. "github.com/tendermint/go-rpc/types" events "github.com/tendermint/go-events"
"github.com/tendermint/go-wire" 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. // 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 return
} }
var request RPCRequest var request types.RPCRequest
err := json.Unmarshal(b, &request) err := json.Unmarshal(b, &request)
if err != nil { 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 return
} }
if len(r.URL.Path) > 1 { 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 return
} }
rpcFunc := funcMap[request.Method] rpcFunc := funcMap[request.Method]
if rpcFunc == nil { 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 return
} }
if rpcFunc.ws { 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 return
} }
args, err := jsonParamsToArgs(rpcFunc, request.Params) args, err := jsonParamsToArgsRPC(rpcFunc, request.Params)
if err != nil { 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 return
} }
returns := rpcFunc.f.Call(args) returns := rpcFunc.f.Call(args)
log.Info("HTTPJSONRPC", "method", request.Method, "args", args, "returns", returns) log.Info("HTTPJSONRPC", "method", request.Method, "args", args, "returns", returns)
result, err := unreflectResult(returns) result, err := unreflectResult(returns)
if err != nil { 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 return
} }
WriteRPCResponseHTTP(w, NewRPCResponse(request.ID, result, "")) WriteRPCResponseHTTP(w, types.NewRPCResponse(request.ID, result, ""))
} }
} }
// Convert a list of interfaces to properly typed values // Convert a []interface{} OR a map[string]interface{} to properly typed values
func jsonParamsToArgs(rpcFunc *RPCFunc, params []interface{}) ([]reflect.Value, error) { //
if len(rpcFunc.argNames) != len(params) { // argsOffset should be 0 for RPC calls, and 1 for WS requests, where len(rpcFunc.args) != len(rpcFunc.argNames).
return nil, errors.New(fmt.Sprintf("Expected %v parameters (%v), got %v (%v)", // Example:
len(rpcFunc.argNames), rpcFunc.argNames, len(params), params)) // rpcFunc.args = [rpctypes.WSRPCContext string]
} // rpcFunc.argNames = ["arg"]
values := make([]reflect.Value, len(params)) func jsonParamsToArgs(rpcFunc *RPCFunc, paramsI interface{}, argsOffset int) ([]reflect.Value, error) {
for i, p := range params { values := make([]reflect.Value, len(rpcFunc.argNames))
ty := rpcFunc.args[i]
v, err := _jsonObjectToArg(ty, p) switch params := paramsI.(type) {
if err != nil {
return nil, err 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 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 // 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, paramsI interface{}, wsCtx types.WSRPCContext) ([]reflect.Value, error) {
if len(rpcFunc.argNames) != len(params) { values, err := jsonParamsToArgs(rpcFunc, paramsI, 1)
return nil, errors.New(fmt.Sprintf("Expected %v parameters (%v), got %v (%v)", if err != nil {
len(rpcFunc.argNames)-1, rpcFunc.argNames[1:], len(params), params)) return nil, err
} }
values := make([]reflect.Value, len(params)+1) return append([]reflect.Value{reflect.ValueOf(wsCtx)}, values...), nil
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
} }
func _jsonObjectToArg(ty reflect.Type, object interface{}) (reflect.Value, error) { 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 // Exception for websocket endpoints
if rpcFunc.ws { if rpcFunc.ws {
return func(w http.ResponseWriter, r *http.Request) { 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 // All other endpoints
@ -205,33 +229,38 @@ func makeHTTPHandler(rpcFunc *RPCFunc) func(http.ResponseWriter, *http.Request)
log.Debug("HTTP HANDLER", "req", r) log.Debug("HTTP HANDLER", "req", r)
args, err := httpParamsToArgs(rpcFunc, r) args, err := httpParamsToArgs(rpcFunc, r)
if err != nil { 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 return
} }
returns := rpcFunc.f.Call(args) returns := rpcFunc.f.Call(args)
log.Info("HTTPRestRPC", "method", r.URL.Path, "args", args, "returns", returns) log.Info("HTTPRestRPC", "method", r.URL.Path, "args", args, "returns", returns)
result, err := unreflectResult(returns) result, err := unreflectResult(returns)
if err != nil { if err != nil {
WriteRPCResponseHTTP(w, NewRPCResponse("", nil, fmt.Sprintf("Error unreflecting result: %v", err.Error()))) WriteRPCResponseHTTP(w, types.NewRPCResponse("", nil, err.Error()))
return return
} }
WriteRPCResponseHTTP(w, NewRPCResponse("", result, "")) WriteRPCResponseHTTP(w, types.NewRPCResponse("", result, ""))
} }
} }
// Covert an http query to a list of properly typed values. // 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). // 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) { func httpParamsToArgs(rpcFunc *RPCFunc, r *http.Request) ([]reflect.Value, error) {
argTypes := rpcFunc.args values := make([]reflect.Value, len(rpcFunc.args))
argNames := rpcFunc.argNames
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) 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 { if err != nil {
return nil, err return nil, err
} }
@ -241,11 +270,12 @@ func httpParamsToArgs(rpcFunc *RPCFunc, r *http.Request) ([]reflect.Value, error
} }
// Pass values to go-wire // Pass values to go-wire
values[i], err = _jsonStringToArg(ty, arg) values[i], err = _jsonStringToArg(argType, arg)
if err != nil { if err != nil {
return nil, err return nil, err
} }
} }
return values, nil return values, nil
} }
@ -268,7 +298,7 @@ func nonJsonToArg(ty reflect.Type, arg string) (reflect.Value, error, bool) {
if isHexString { if isHexString {
if !expectingString && !expectingByteSlice { 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()) ty.Kind().String())
return reflect.ValueOf(nil), err, false return reflect.ValueOf(nil), err, false
} }
@ -313,11 +343,11 @@ const (
// contains listener id, underlying ws connection, // contains listener id, underlying ws connection,
// and the event switch for subscribing to events // and the event switch for subscribing to events
type wsConnection struct { type wsConnection struct {
BaseService cmn.BaseService
remoteAddr string remoteAddr string
baseConn *websocket.Conn baseConn *websocket.Conn
writeChan chan RPCResponse writeChan chan types.RPCResponse
readTimeout *time.Timer readTimeout *time.Timer
pingTicker *time.Ticker pingTicker *time.Ticker
@ -330,11 +360,11 @@ func NewWSConnection(baseConn *websocket.Conn, funcMap map[string]*RPCFunc, evsw
wsc := &wsConnection{ wsc := &wsConnection{
remoteAddr: baseConn.RemoteAddr().String(), remoteAddr: baseConn.RemoteAddr().String(),
baseConn: baseConn, baseConn: baseConn,
writeChan: make(chan RPCResponse, writeChanCapacity), // error when full. writeChan: make(chan types.RPCResponse, writeChanCapacity), // error when full.
funcMap: funcMap, funcMap: funcMap,
evsw: evsw, evsw: evsw,
} }
wsc.BaseService = *NewBaseService(log, "wsConnection", wsc) wsc.BaseService = *cmn.NewBaseService(log, "wsConnection", wsc)
return wsc return wsc
} }
@ -342,12 +372,15 @@ func NewWSConnection(baseConn *websocket.Conn, funcMap map[string]*RPCFunc, evsw
func (wsc *wsConnection) OnStart() error { func (wsc *wsConnection) OnStart() error {
wsc.BaseService.OnStart() 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 // Read subscriptions/unsubscriptions to events
go wsc.readRoutine() go wsc.readRoutine()
// Custom Ping handler to touch readTimeout // 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 { wsc.baseConn.SetPingHandler(func(m string) error {
// NOTE: https://github.com/gorilla/websocket/issues/97 // NOTE: https://github.com/gorilla/websocket/issues/97
go wsc.baseConn.WriteControl(websocket.PongMessage, []byte(m), time.Now().Add(time.Second*wsWriteTimeoutSeconds)) 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() { func (wsc *wsConnection) OnStop() {
wsc.BaseService.OnStop() wsc.BaseService.OnStop()
wsc.evsw.RemoveListener(wsc.remoteAddr) if wsc.evsw != nil {
wsc.evsw.RemoveListener(wsc.remoteAddr)
}
wsc.readTimeout.Stop() wsc.readTimeout.Stop()
wsc.pingTicker.Stop() wsc.pingTicker.Stop()
// The write loop closes the websocket connection // The write loop closes the websocket connection
@ -399,7 +434,7 @@ func (wsc *wsConnection) GetEventSwitch() events.EventSwitch {
// Implements WSRPCConnection // Implements WSRPCConnection
// Blocking write to writeChan until service stops. // Blocking write to writeChan until service stops.
// Goroutine-safe // Goroutine-safe
func (wsc *wsConnection) WriteRPCResponse(resp RPCResponse) { func (wsc *wsConnection) WriteRPCResponse(resp types.RPCResponse) {
select { select {
case <-wsc.Quit: case <-wsc.Quit:
return return
@ -410,7 +445,7 @@ func (wsc *wsConnection) WriteRPCResponse(resp RPCResponse) {
// Implements WSRPCConnection // Implements WSRPCConnection
// Nonblocking write. // Nonblocking write.
// Goroutine-safe // Goroutine-safe
func (wsc *wsConnection) TryWriteRPCResponse(resp RPCResponse) bool { func (wsc *wsConnection) TryWriteRPCResponse(resp types.RPCResponse) bool {
select { select {
case <-wsc.Quit: case <-wsc.Quit:
return false return false
@ -444,11 +479,11 @@ func (wsc *wsConnection) readRoutine() {
wsc.Stop() wsc.Stop()
return return
} }
var request RPCRequest var request types.RPCRequest
err = json.Unmarshal(in, &request) err = json.Unmarshal(in, &request)
if err != nil { if err != nil {
errStr := fmt.Sprintf("Error unmarshaling data: %s", err.Error()) 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 continue
} }
@ -456,28 +491,28 @@ func (wsc *wsConnection) readRoutine() {
rpcFunc := wsc.funcMap[request.Method] rpcFunc := wsc.funcMap[request.Method]
if rpcFunc == nil { 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 continue
} }
var args []reflect.Value var args []reflect.Value
if rpcFunc.ws { if rpcFunc.ws {
wsCtx := WSRPCContext{Request: request, WSRPCConnection: wsc} wsCtx := types.WSRPCContext{Request: request, WSRPCConnection: wsc}
args, err = jsonParamsToArgsWS(rpcFunc, request.Params, wsCtx) args, err = jsonParamsToArgsWS(rpcFunc, request.Params, wsCtx)
} else { } else {
args, err = jsonParamsToArgs(rpcFunc, request.Params) args, err = jsonParamsToArgsRPC(rpcFunc, request.Params)
} }
if err != nil { if err != nil {
wsc.WriteRPCResponse(NewRPCResponse(request.ID, nil, err.Error())) wsc.WriteRPCResponse(types.NewRPCResponse(request.ID, nil, err.Error()))
continue continue
} }
returns := rpcFunc.f.Call(args) returns := rpcFunc.f.Call(args)
log.Info("WSJSONRPC", "method", request.Method, "args", args, "returns", returns) log.Info("WSJSONRPC", "method", request.Method, "args", args, "returns", returns)
result, err := unreflectResult(returns) result, err := unreflectResult(returns)
if err != nil { if err != nil {
wsc.WriteRPCResponse(NewRPCResponse(request.ID, nil, err.Error())) wsc.WriteRPCResponse(types.NewRPCResponse(request.ID, nil, err.Error()))
continue continue
} else { } else {
wsc.WriteRPCResponse(NewRPCResponse(request.ID, result, "")) wsc.WriteRPCResponse(types.NewRPCResponse(request.ID, result, ""))
continue continue
} }
@ -563,7 +598,7 @@ func (wm *WebsocketManager) WebsocketHandler(w http.ResponseWriter, r *http.Requ
func unreflectResult(returns []reflect.Value) (interface{}, error) { func unreflectResult(returns []reflect.Value) (interface{}, error) {
errV := returns[1] errV := returns[1]
if errV.Interface() != nil { if errV.Interface() != nil {
return nil, fmt.Errorf("%v", errV.Interface()) return nil, errors.Errorf("%v", errV.Interface())
} }
rv := returns[0] rv := returns[0]
// the result is a registered interface, // the result is a registered interface,

View File

@ -2,10 +2,11 @@ package rpcserver
import ( import (
"encoding/hex" "encoding/hex"
"fmt"
"net/http" "net/http"
"regexp" "regexp"
"strconv" "strconv"
"github.com/pkg/errors"
) )
var ( var (
@ -39,7 +40,7 @@ func GetParamInt64(r *http.Request, param string) (int64, error) {
s := GetParam(r, param) s := GetParam(r, param)
i, err := strconv.ParseInt(s, 10, 64) i, err := strconv.ParseInt(s, 10, 64)
if err != nil { if err != nil {
return 0, fmt.Errorf(param, err.Error()) return 0, errors.Errorf(param, err.Error())
} }
return i, nil return i, nil
} }
@ -48,7 +49,7 @@ func GetParamInt32(r *http.Request, param string) (int32, error) {
s := GetParam(r, param) s := GetParam(r, param)
i, err := strconv.ParseInt(s, 10, 32) i, err := strconv.ParseInt(s, 10, 32)
if err != nil { if err != nil {
return 0, fmt.Errorf(param, err.Error()) return 0, errors.Errorf(param, err.Error())
} }
return int32(i), nil return int32(i), nil
} }
@ -57,7 +58,7 @@ func GetParamUint64(r *http.Request, param string) (uint64, error) {
s := GetParam(r, param) s := GetParam(r, param)
i, err := strconv.ParseUint(s, 10, 64) i, err := strconv.ParseUint(s, 10, 64)
if err != nil { if err != nil {
return 0, fmt.Errorf(param, err.Error()) return 0, errors.Errorf(param, err.Error())
} }
return i, nil return i, nil
} }
@ -66,7 +67,7 @@ func GetParamUint(r *http.Request, param string) (uint, error) {
s := GetParam(r, param) s := GetParam(r, param)
i, err := strconv.ParseUint(s, 10, 64) i, err := strconv.ParseUint(s, 10, 64)
if err != nil { if err != nil {
return 0, fmt.Errorf(param, err.Error()) return 0, errors.Errorf(param, err.Error())
} }
return uint(i), nil 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) { func GetParamRegexp(r *http.Request, param string, re *regexp.Regexp) (string, error) {
s := GetParam(r, param) s := GetParam(r, param)
if !re.MatchString(s) { 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 return s, nil
} }
@ -83,7 +84,7 @@ func GetParamFloat64(r *http.Request, param string) (float64, error) {
s := GetParam(r, param) s := GetParam(r, param)
f, err := strconv.ParseFloat(s, 64) f, err := strconv.ParseFloat(s, 64)
if err != nil { if err != nil {
return 0, fmt.Errorf(param, err.Error()) return 0, errors.Errorf(param, err.Error())
} }
return f, nil return f, nil
} }

View File

@ -11,9 +11,8 @@ import (
"strings" "strings"
"time" "time"
. "github.com/tendermint/go-common" "github.com/pkg/errors"
. "github.com/tendermint/go-rpc/types" types "github.com/tendermint/go-rpc/types"
//"github.com/tendermint/go-wire"
) )
func StartHTTPServer(listenAddr string, handler http.Handler) (listener net.Listener, err error) { 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") 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 // we used to allow addrs without tcp/unix prefix by checking for a colon
// TODO: Deprecate // TODO: Deprecate
proto = SocketType(listenAddr) proto = types.SocketType(listenAddr)
addr = listenAddr addr = listenAddr
// return nil, fmt.Errorf("Invalid listener address %s", lisenAddr) // return nil, errors.Errorf("Invalid listener address %s", lisenAddr)
} else { } else {
proto, addr = parts[0], parts[1] 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) listener, err = net.Listen(proto, addr)
if err != nil { 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() { go func() {
@ -47,7 +46,7 @@ func StartHTTPServer(listenAddr string, handler http.Handler) (listener net.List
return listener, nil return listener, nil
} }
func WriteRPCResponseHTTP(w http.ResponseWriter, res RPCResponse) { func WriteRPCResponseHTTP(w http.ResponseWriter, res types.RPCResponse) {
// jsonBytes := wire.JSONBytesPretty(res) // jsonBytes := wire.JSONBytesPretty(res)
jsonBytes, err := json.Marshal(res) jsonBytes, err := json.Marshal(res)
if err != nil { if err != nil {
@ -83,13 +82,13 @@ func RecoverAndLogHandler(handler http.Handler) http.Handler {
if e := recover(); e != nil { if e := recover(); e != nil {
// If RPCResponse // If RPCResponse
if res, ok := e.(RPCResponse); ok { if res, ok := e.(types.RPCResponse); ok {
WriteRPCResponseHTTP(rww, res) WriteRPCResponseHTTP(rww, res)
} else { } else {
// For the rest, // For the rest,
log.Error("Panic in RPC HTTP handler", "error", e, "stack", string(debug.Stack())) log.Error("Panic in RPC HTTP handler", "error", e, "stack", string(debug.Stack()))
rww.WriteHeader(http.StatusInternalServerError) 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)))
} }
} }

View File

@ -1,6 +1,9 @@
{ {
"jsonrpc":"2.0", "jsonrpc": "2.0",
"id":"", "id": "",
"method":"hello_world", "method": "hello_world",
"params":["my_world", 5] "params": {
"name": "my_world",
"num": 5
}
} }

95
test/integration_test.sh Executable file
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

View File

@ -4,7 +4,7 @@ import (
"fmt" "fmt"
"net/http" "net/http"
. "github.com/tendermint/go-common" cmn "github.com/tendermint/go-common"
rpcserver "github.com/tendermint/go-rpc/server" rpcserver "github.com/tendermint/go-rpc/server"
) )
@ -25,11 +25,11 @@ func main() {
rpcserver.RegisterRPCFuncs(mux, routes) rpcserver.RegisterRPCFuncs(mux, routes)
_, err := rpcserver.StartHTTPServer("0.0.0.0:8008", mux) _, err := rpcserver.StartHTTPServer("0.0.0.0:8008", mux)
if err != nil { if err != nil {
Exit(err.Error()) cmn.Exit(err.Error())
} }
// Wait forever // Wait forever
TrapSignal(func() { cmn.TrapSignal(func() {
}) })
} }

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

View File

@ -4,18 +4,18 @@ import (
"encoding/json" "encoding/json"
"strings" "strings"
"github.com/tendermint/go-events" events "github.com/tendermint/go-events"
"github.com/tendermint/go-wire" wire "github.com/tendermint/go-wire"
) )
type RPCRequest struct { type RPCRequest struct {
JSONRPC string `json:"jsonrpc"` JSONRPC string `json:"jsonrpc"`
ID string `json:"id"` ID string `json:"id"`
Method string `json:"method"` Method string `json:"method"`
Params []interface{} `json:"params"` 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{ return RPCRequest{
JSONRPC: "2.0", JSONRPC: "2.0",
ID: id, ID: id,

View File

@ -1,7 +1,7 @@
package rpc package rpc
const Maj = "0" const Maj = "0"
const Min = "6" // 0x-prefixed string args handled as hex const Min = "7"
const Fix = "0" // const Fix = "0"
const Version = Maj + "." + Min + "." + Fix const Version = Maj + "." + Min + "." + Fix