From c52524a215d2cc90f718172f06139b53db0ec87c Mon Sep 17 00:00:00 2001 From: Ethan Buchman Date: Tue, 12 Jan 2016 15:26:00 -0500 Subject: [PATCH 01/73] Initial commit --- LICENSE | 201 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 1 + 2 files changed, 202 insertions(+) create mode 100644 LICENSE create mode 100644 README.md diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..8dada3eda --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 000000000..fd01b380c --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# go-rpc \ No newline at end of file From 3d59e13dd8562331fd04cdc99ff755898ec44d5c Mon Sep 17 00:00:00 2001 From: Ethan Buchman Date: Tue, 12 Jan 2016 16:50:06 -0500 Subject: [PATCH 02/73] move from tendermint/tendermint --- README.md | 5 +- client/http_client.go | 133 ++++++++++ client/log.go | 7 + client/ws_client.go | 119 +++++++++ server/handlers.go | 553 ++++++++++++++++++++++++++++++++++++++++++ server/http_params.go | 89 +++++++ server/http_server.go | 115 +++++++++ server/log.go | 7 + types/types.go | 71 ++++++ version.go | 3 + 10 files changed, 1101 insertions(+), 1 deletion(-) create mode 100644 client/http_client.go create mode 100644 client/log.go create mode 100644 client/ws_client.go create mode 100644 server/handlers.go create mode 100644 server/http_params.go create mode 100644 server/http_server.go create mode 100644 server/log.go create mode 100644 types/types.go create mode 100644 version.go diff --git a/README.md b/README.md index fd01b380c..e74cf8021 100644 --- a/README.md +++ b/README.md @@ -1 +1,4 @@ -# go-rpc \ No newline at end of file +# go-rpc + +HTTP RPC server supporting calls via uri params, jsonrpc, and jsonrpc over websockets + diff --git a/client/http_client.go b/client/http_client.go new file mode 100644 index 000000000..133f2b725 --- /dev/null +++ b/client/http_client.go @@ -0,0 +1,133 @@ +package rpcclient + +import ( + "bytes" + "errors" + "io/ioutil" + "net/http" + "net/url" + "strings" + + . "github.com/tendermint/go-common" + "github.com/tendermint/go-rpc/types" + "github.com/tendermint/go-wire" +) + +// JSON rpc takes params as a slice +type ClientJSONRPC struct { + remote string +} + +func NewClientJSONRPC(remote string) *ClientJSONRPC { + return &ClientJSONRPC{remote} +} + +func (c *ClientJSONRPC) Call(method string, params []interface{}) (interface{}, error) { + return CallHTTP_JSONRPC(c.remote, method, params) +} + +// URI takes params as a map +type ClientURI struct { + remote string +} + +func NewClientURI(remote string) *ClientURI { + if !strings.HasSuffix(remote, "/") { + remote = remote + "/" + } + return &ClientURI{remote} +} + +func (c *ClientURI) Call(method string, params map[string]interface{}) (interface{}, error) { + return CallHTTP_URI(c.remote, method, params) +} + +func CallHTTP_JSONRPC(remote string, method string, params []interface{}) (interface{}, error) { + // Make request and get responseBytes + request := rpctypes.RPCRequest{ + JSONRPC: "2.0", + Method: method, + Params: params, + ID: "", + } + requestBytes := wire.JSONBytes(request) + requestBuf := bytes.NewBuffer(requestBytes) + log.Info(Fmt("RPC request to %v: %v", remote, string(requestBytes))) + httpResponse, err := http.Post(remote, "text/json", requestBuf) + if err != nil { + return nil, err + } + defer httpResponse.Body.Close() + responseBytes, err := ioutil.ReadAll(httpResponse.Body) + if err != nil { + return nil, err + } + log.Info(Fmt("RPC response: %v", string(responseBytes))) + return unmarshalResponseBytes(responseBytes) +} + +func CallHTTP_URI(remote string, method string, params map[string]interface{}) (interface{}, error) { + values, err := argsToURLValues(params) + if err != nil { + return nil, err + } + log.Info(Fmt("URI request to %v: %v", remote, values)) + resp, err := http.PostForm(remote+method, values) + if err != nil { + return nil, err + } + defer resp.Body.Close() + responseBytes, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + return unmarshalResponseBytes(responseBytes) +} + +//------------------------------------------------ + +func unmarshalResponseBytes(responseBytes []byte) (interface{}, error) { + // read response + // if rpc/core/types is imported, the result will unmarshal + // into the correct type + var err error + response := &rpctypes.RPCResponse{} + wire.ReadJSON(response, responseBytes, &err) + if err != nil { + return nil, err + } + errorStr := response.Error + if errorStr != "" { + return nil, errors.New(errorStr) + } + return response.Result, err +} + +func argsToURLValues(args map[string]interface{}) (url.Values, error) { + values := make(url.Values) + if len(args) == 0 { + return values, nil + } + err := argsToJson(args) + if err != nil { + return nil, err + } + for key, val := range args { + values.Set(key, val.(string)) + } + return values, nil +} + +func argsToJson(args map[string]interface{}) error { + var n int + var err error + for k, v := range args { + buf := new(bytes.Buffer) + wire.WriteJSON(v, buf, &n, &err) + if err != nil { + return err + } + args[k] = buf.String() + } + return nil +} diff --git a/client/log.go b/client/log.go new file mode 100644 index 000000000..8b33e2f10 --- /dev/null +++ b/client/log.go @@ -0,0 +1,7 @@ +package rpcclient + +import ( + "github.com/tendermint/log15" +) + +var log = log15.New("module", "rpcclient") diff --git a/client/ws_client.go b/client/ws_client.go new file mode 100644 index 000000000..ae2b324a4 --- /dev/null +++ b/client/ws_client.go @@ -0,0 +1,119 @@ +package rpcclient + +import ( + "net/http" + "time" + + "github.com/gorilla/websocket" + . "github.com/tendermint/go-common" + "github.com/tendermint/go-rpc/types" + "github.com/tendermint/go-wire" +) + +const ( + wsResultsChannelCapacity = 10 + wsWriteTimeoutSeconds = 10 +) + +type WSClient struct { + QuitService + Address string + *websocket.Conn + ResultsCh chan rpctypes.Result // closes upon WSClient.Stop() +} + +// create a new connection +func NewWSClient(addr string) *WSClient { + wsClient := &WSClient{ + Address: addr, + Conn: nil, + ResultsCh: make(chan rpctypes.Result, wsResultsChannelCapacity), + } + wsClient.QuitService = *NewQuitService(log, "WSClient", wsClient) + return wsClient +} + +func (wsc *WSClient) OnStart() error { + wsc.QuitService.OnStart() + err := wsc.dial() + if err != nil { + return err + } + go wsc.receiveEventsRoutine() + return nil +} + +func (wsc *WSClient) dial() error { + // Dial + dialer := websocket.DefaultDialer + rHeader := http.Header{} + con, _, err := dialer.Dial(wsc.Address, rHeader) + if err != nil { + return err + } + // Set the ping/pong handlers + con.SetPingHandler(func(m string) error { + // NOTE: https://github.com/gorilla/websocket/issues/97 + log.Debug("Client received ping, writing pong") + go con.WriteControl(websocket.PongMessage, []byte(m), time.Now().Add(time.Second*wsWriteTimeoutSeconds)) + return nil + }) + con.SetPongHandler(func(m string) error { + log.Debug("Client received pong") + // NOTE: https://github.com/gorilla/websocket/issues/97 + return nil + }) + wsc.Conn = con + return nil +} + +func (wsc *WSClient) OnStop() { + wsc.QuitService.OnStop() + // ResultsCh is closed in receiveEventsRoutine. +} + +func (wsc *WSClient) receiveEventsRoutine() { + for { + log.Notice("Waiting for wsc message ...") + _, data, err := wsc.ReadMessage() + if err != nil { + log.Info("WSClient failed to read message", "error", err, "data", string(data)) + wsc.Stop() + break + } else { + var response rpctypes.RPCResponse + wire.ReadJSON(&response, data, &err) + if err != nil { + log.Info("WSClient failed to parse message", "error", err, "data", string(data)) + wsc.Stop() + break + } + wsc.ResultsCh <- response.Result + } + } + + // Cleanup + close(wsc.ResultsCh) +} + +// subscribe to an event +func (wsc *WSClient) Subscribe(eventid string) error { + err := wsc.WriteJSON(rpctypes.RPCRequest{ + JSONRPC: "2.0", + ID: "", + Method: "subscribe", + Params: []interface{}{eventid}, + }) + return err +} + +// unsubscribe from an event +func (wsc *WSClient) Unsubscribe(eventid string) error { + err := wsc.WriteJSON(rpctypes.RPCRequest{ + JSONRPC: "2.0", + ID: "", + Method: "unsubscribe", + Params: []interface{}{eventid}, + }) + return err +} diff --git a/server/handlers.go b/server/handlers.go new file mode 100644 index 000000000..64c151808 --- /dev/null +++ b/server/handlers.go @@ -0,0 +1,553 @@ +package rpcserver + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "net/http" + "reflect" + "sort" + "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" +) + +func RegisterRPCFuncs(mux *http.ServeMux, funcMap map[string]*RPCFunc) { + // HTTP endpoints + for funcName, rpcFunc := range funcMap { + mux.HandleFunc("/"+funcName, makeHTTPHandler(rpcFunc)) + } + + // JSONRPC endpoints + mux.HandleFunc("/", makeJSONRPCHandler(funcMap)) +} + +//------------------------------------- +// function introspection + +// holds all type information for each function +type RPCFunc struct { + f reflect.Value // underlying rpc function + args []reflect.Type // type of each function arg + returns []reflect.Type // type of each return arg + argNames []string // name of each argument + ws bool // websocket only +} + +// wraps a function for quicker introspection +func NewRPCFunc(f interface{}, args []string) *RPCFunc { + return &RPCFunc{ + f: reflect.ValueOf(f), + args: funcArgTypes(f), + returns: funcReturnTypes(f), + argNames: args, + ws: false, + } +} + +func NewWSRPCFunc(f interface{}, args []string) *RPCFunc { + return &RPCFunc{ + f: reflect.ValueOf(f), + args: funcArgTypes(f), + returns: funcReturnTypes(f), + argNames: args, + ws: true, + } +} + +// return a function's argument types +func funcArgTypes(f interface{}) []reflect.Type { + t := reflect.TypeOf(f) + n := t.NumIn() + typez := make([]reflect.Type, n) + for i := 0; i < n; i++ { + typez[i] = t.In(i) + } + return typez +} + +// return a function's return types +func funcReturnTypes(f interface{}) []reflect.Type { + t := reflect.TypeOf(f) + n := t.NumOut() + typez := make([]reflect.Type, n) + for i := 0; i < n; i++ { + typez[i] = t.Out(i) + } + return typez +} + +// function introspection +//----------------------------------------------------------------------------- +// rpc.json + +// jsonrpc calls grab the given method's function info and runs reflect.Call +func makeJSONRPCHandler(funcMap map[string]*RPCFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + b, _ := ioutil.ReadAll(r.Body) + // if its an empty request (like from a browser), + // just display a list of functions + if len(b) == 0 { + writeListOfEndpoints(w, r, funcMap) + return + } + + var request RPCRequest + err := json.Unmarshal(b, &request) + if err != nil { + WriteRPCResponseHTTP(w, NewRPCResponse("", nil, err.Error())) + return + } + if len(r.URL.Path) > 1 { + WriteRPCResponseHTTP(w, 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)) + return + } + if rpcFunc.ws { + WriteRPCResponseHTTP(w, NewRPCResponse(request.ID, nil, "RPC method is only for websockets: "+request.Method)) + return + } + args, err := jsonParamsToArgs(rpcFunc, request.Params) + if err != nil { + WriteRPCResponseHTTP(w, NewRPCResponse(request.ID, nil, 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, nil, err.Error())) + return + } + WriteRPCResponseHTTP(w, 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 + } + 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 + } + return values, nil +} + +func _jsonObjectToArg(ty reflect.Type, object interface{}) (reflect.Value, error) { + var err error + v := reflect.New(ty) + wire.ReadJSONObjectPtr(v.Interface(), object, &err) + if err != nil { + return v, err + } + v = v.Elem() + return v, nil +} + +// rpc.json +//----------------------------------------------------------------------------- +// rpc.http + +// convert from a function name to the http handler +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")) + } + } + // All other endpoints + return func(w http.ResponseWriter, r *http.Request) { + args, err := httpParamsToArgs(rpcFunc, r) + if err != nil { + WriteRPCResponseHTTP(w, NewRPCResponse("", nil, 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, err.Error())) + return + } + WriteRPCResponseHTTP(w, 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 + + var err error + values := make([]reflect.Value, len(argNames)) + for i, name := range argNames { + ty := argTypes[i] + arg := GetParam(r, name) + values[i], err = _jsonStringToArg(ty, arg) + if err != nil { + return nil, err + } + } + return values, nil +} + +func _jsonStringToArg(ty reflect.Type, arg string) (reflect.Value, error) { + var err error + v := reflect.New(ty) + wire.ReadJSONPtr(v.Interface(), []byte(arg), &err) + if err != nil { + return v, err + } + v = v.Elem() + return v, nil +} + +// rpc.http +//----------------------------------------------------------------------------- +// rpc.websocket + +const ( + writeChanCapacity = 1000 + wsWriteTimeoutSeconds = 30 // each write times out after this + wsReadTimeoutSeconds = 30 // connection times out if we haven't received *anything* in this long, not even pings. + wsPingTickerSeconds = 10 // send a ping every PingTickerSeconds. +) + +// a single websocket connection +// contains listener id, underlying ws connection, +// and the event switch for subscribing to events +type wsConnection struct { + QuitService + + remoteAddr string + baseConn *websocket.Conn + writeChan chan RPCResponse + readTimeout *time.Timer + pingTicker *time.Ticker + + funcMap map[string]*RPCFunc + evsw *events.EventSwitch +} + +// new websocket connection wrapper +func NewWSConnection(baseConn *websocket.Conn, funcMap map[string]*RPCFunc, evsw *events.EventSwitch) *wsConnection { + wsc := &wsConnection{ + remoteAddr: baseConn.RemoteAddr().String(), + baseConn: baseConn, + writeChan: make(chan RPCResponse, writeChanCapacity), // error when full. + funcMap: funcMap, + evsw: evsw, + } + wsc.QuitService = *NewQuitService(log, "wsConnection", wsc) + return wsc +} + +// wsc.Start() blocks until the connection closes. +func (wsc *wsConnection) OnStart() error { + wsc.QuitService.OnStart() + + // Read subscriptions/unsubscriptions to events + go wsc.readRoutine() + + // Custom Ping handler to touch readTimeout + wsc.readTimeout = time.NewTimer(time.Second * wsReadTimeoutSeconds) + wsc.pingTicker = time.NewTicker(time.Second * wsPingTickerSeconds) + wsc.baseConn.SetPingHandler(func(m string) error { + // NOTE: https://github.com/gorilla/websocket/issues/97 + go wsc.baseConn.WriteControl(websocket.PongMessage, []byte(m), time.Now().Add(time.Second*wsWriteTimeoutSeconds)) + wsc.readTimeout.Reset(time.Second * wsReadTimeoutSeconds) + return nil + }) + wsc.baseConn.SetPongHandler(func(m string) error { + // NOTE: https://github.com/gorilla/websocket/issues/97 + wsc.readTimeout.Reset(time.Second * wsReadTimeoutSeconds) + return nil + }) + go wsc.readTimeoutRoutine() + + // Write responses, BLOCKING. + wsc.writeRoutine() + return nil +} + +func (wsc *wsConnection) OnStop() { + wsc.QuitService.OnStop() + wsc.evsw.RemoveListener(wsc.remoteAddr) + wsc.readTimeout.Stop() + wsc.pingTicker.Stop() + // The write loop closes the websocket connection + // when it exits its loop, and the read loop + // closes the writeChan +} + +func (wsc *wsConnection) readTimeoutRoutine() { + select { + case <-wsc.readTimeout.C: + log.Notice("Stopping connection due to read timeout") + wsc.Stop() + case <-wsc.Quit: + return + } +} + +// Implements WSRPCConnection +func (wsc *wsConnection) GetRemoteAddr() string { + return wsc.remoteAddr +} + +// Implements WSRPCConnection +func (wsc *wsConnection) GetEventSwitch() *events.EventSwitch { + return wsc.evsw +} + +// Implements WSRPCConnection +// Blocking write to writeChan until service stops. +func (wsc *wsConnection) WriteRPCResponse(resp RPCResponse) { + select { + case <-wsc.Quit: + return + case wsc.writeChan <- resp: + } +} + +// Implements WSRPCConnection +// Nonblocking write. +func (wsc *wsConnection) TryWriteRPCResponse(resp RPCResponse) bool { + select { + case <-wsc.Quit: + return false + case wsc.writeChan <- resp: + return true + default: + return false + } +} + +// Read from the socket and subscribe to or unsubscribe from events +func (wsc *wsConnection) readRoutine() { + // Do not close writeChan, to allow WriteRPCResponse() to fail. + // defer close(wsc.writeChan) + + for { + select { + case <-wsc.Quit: + return + default: + var in []byte + // Do not set a deadline here like below: + // wsc.baseConn.SetReadDeadline(time.Now().Add(time.Second * wsReadTimeoutSeconds)) + // The client may not send anything for a while. + // We use `readTimeout` to handle read timeouts. + _, in, err := wsc.baseConn.ReadMessage() + if err != nil { + log.Notice("Failed to read from connection", "remote", wsc.remoteAddr) + // an error reading the connection, + // kill the connection + wsc.Stop() + return + } + var request 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)) + continue + } + + // Now, fetch the RPCFunc and execute it. + + rpcFunc := wsc.funcMap[request.Method] + if rpcFunc == nil { + wsc.WriteRPCResponse(NewRPCResponse(request.ID, nil, "RPC method unknown: "+request.Method)) + continue + } + var args []reflect.Value + if rpcFunc.ws { + wsCtx := WSRPCContext{Request: request, WSRPCConnection: wsc} + args, err = jsonParamsToArgsWS(rpcFunc, request.Params, wsCtx) + } else { + args, err = jsonParamsToArgs(rpcFunc, request.Params) + } + if err != nil { + wsc.WriteRPCResponse(NewRPCResponse(request.ID, nil, err.Error())) + 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())) + continue + } else { + wsc.WriteRPCResponse(NewRPCResponse(request.ID, result, "")) + continue + } + + } + } +} + +// receives on a write channel and writes out on the socket +func (wsc *wsConnection) writeRoutine() { + defer wsc.baseConn.Close() + var n, err = int(0), error(nil) + for { + select { + case <-wsc.Quit: + return + case <-wsc.pingTicker.C: + err := wsc.baseConn.WriteMessage(websocket.PingMessage, []byte{}) + if err != nil { + log.Error("Failed to write ping message on websocket", "error", err) + wsc.Stop() + return + } + case msg := <-wsc.writeChan: + buf := new(bytes.Buffer) + wire.WriteJSON(msg, buf, &n, &err) + if err != nil { + log.Error("Failed to marshal RPCResponse to JSON", "error", err) + } else { + wsc.baseConn.SetWriteDeadline(time.Now().Add(time.Second * wsWriteTimeoutSeconds)) + bufBytes := buf.Bytes() + if err = wsc.baseConn.WriteMessage(websocket.TextMessage, bufBytes); err != nil { + log.Warn("Failed to write response on websocket", "error", err) + wsc.Stop() + return + } + } + } + } +} + +//---------------------------------------- + +// Main manager for all websocket connections +// Holds the event switch +// NOTE: The websocket path is defined externally, e.g. in node/node.go +type WebsocketManager struct { + websocket.Upgrader + funcMap map[string]*RPCFunc + evsw *events.EventSwitch +} + +func NewWebsocketManager(funcMap map[string]*RPCFunc, evsw *events.EventSwitch) *WebsocketManager { + return &WebsocketManager{ + funcMap: funcMap, + evsw: evsw, + Upgrader: websocket.Upgrader{ + ReadBufferSize: 1024, + WriteBufferSize: 1024, + CheckOrigin: func(r *http.Request) bool { + // TODO + return true + }, + }, + } +} + +// Upgrade the request/response (via http.Hijack) and starts the wsConnection. +func (wm *WebsocketManager) WebsocketHandler(w http.ResponseWriter, r *http.Request) { + wsConn, err := wm.Upgrade(w, r, nil) + if err != nil { + // TODO - return http error + log.Error("Failed to upgrade to websocket connection", "error", err) + return + } + + // register connection + con := NewWSConnection(wsConn, wm.funcMap, wm.evsw) + log.Notice("New websocket connection", "remote", con.remoteAddr) + con.Start() // Blocking +} + +// rpc.websocket +//----------------------------------------------------------------------------- + +// returns is result struct and error. If error is not nil, return it +func unreflectResult(returns []reflect.Value) (interface{}, error) { + errV := returns[1] + if errV.Interface() != nil { + return nil, fmt.Errorf("%v", errV.Interface()) + } + return returns[0].Interface(), nil +} + +// writes a list of available rpc endpoints as an html page +func writeListOfEndpoints(w http.ResponseWriter, r *http.Request, funcMap map[string]*RPCFunc) { + noArgNames := []string{} + argNames := []string{} + for name, funcData := range funcMap { + if len(funcData.args) == 0 { + noArgNames = append(noArgNames, name) + } else { + argNames = append(argNames, name) + } + } + sort.Strings(noArgNames) + sort.Strings(argNames) + buf := new(bytes.Buffer) + buf.WriteString("") + buf.WriteString("
Available endpoints:
") + + for _, name := range noArgNames { + link := fmt.Sprintf("http://%s/%s", r.Host, name) + buf.WriteString(fmt.Sprintf("%s
", link, link)) + } + + buf.WriteString("
Endpoints that require arguments:
") + for _, name := range argNames { + link := fmt.Sprintf("http://%s/%s?", r.Host, name) + funcData := funcMap[name] + for i, argName := range funcData.argNames { + link += argName + "=_" + if i < len(funcData.argNames)-1 { + link += "&" + } + } + buf.WriteString(fmt.Sprintf("%s
", link, link)) + } + buf.WriteString("") + w.Header().Set("Content-Type", "text/html") + w.WriteHeader(200) + w.Write(buf.Bytes()) +} diff --git a/server/http_params.go b/server/http_params.go new file mode 100644 index 000000000..acf5b4c8c --- /dev/null +++ b/server/http_params.go @@ -0,0 +1,89 @@ +package rpcserver + +import ( + "encoding/hex" + "fmt" + "net/http" + "regexp" + "strconv" +) + +var ( + // Parts of regular expressions + atom = "[A-Z0-9!#$%&'*+\\-/=?^_`{|}~]+" + dotAtom = atom + `(?:\.` + atom + `)*` + domain = `[A-Z0-9.-]+\.[A-Z]{2,4}` + + RE_HEX = regexp.MustCompile(`^(?i)[a-f0-9]+$`) + RE_EMAIL = regexp.MustCompile(`^(?i)(` + dotAtom + `)@(` + dotAtom + `)$`) + RE_ADDRESS = regexp.MustCompile(`^(?i)[a-z0-9]{25,34}$`) + RE_HOST = regexp.MustCompile(`^(?i)(` + domain + `)$`) + + //RE_ID12 = regexp.MustCompile(`^[a-zA-Z0-9]{12}$`) +) + +func GetParam(r *http.Request, param string) string { + s := r.URL.Query().Get(param) + if s == "" { + s = r.FormValue(param) + } + return s +} + +func GetParamByteSlice(r *http.Request, param string) ([]byte, error) { + s := GetParam(r, param) + return hex.DecodeString(s) +} + +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 i, nil +} + +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 int32(i), nil +} + +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 i, nil +} + +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 uint(i), nil +} + +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 s, nil +} + +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 f, nil +} diff --git a/server/http_server.go b/server/http_server.go new file mode 100644 index 000000000..37b01ceee --- /dev/null +++ b/server/http_server.go @@ -0,0 +1,115 @@ +// Commons for HTTP handling +package rpcserver + +import ( + "bufio" + "fmt" + "net" + "net/http" + "runtime/debug" + "time" + + "github.com/tendermint/go-alert" + . "github.com/tendermint/go-common" + . "github.com/tendermint/go-rpc/types" + "github.com/tendermint/go-wire" +) + +func StartHTTPServer(listenAddr string, handler http.Handler) (net.Listener, error) { + log.Notice(Fmt("Starting RPC HTTP server on %v", listenAddr)) + listener, err := net.Listen("tcp", listenAddr) + if err != nil { + return nil, fmt.Errorf("Failed to listen to %v", listenAddr) + } + go func() { + res := http.Serve( + listener, + RecoverAndLogHandler(handler), + ) + log.Crit("RPC HTTP server stopped", "result", res) + }() + return listener, nil +} + +func WriteRPCResponseHTTP(w http.ResponseWriter, res RPCResponse) { + jsonBytes := wire.JSONBytesPretty(res) + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + w.Write(jsonBytes) +} + +//----------------------------------------------------------------------------- + +// Wraps an HTTP handler, adding error logging. +// If the inner function panics, the outer function recovers, logs, sends an +// HTTP 500 error response. +func RecoverAndLogHandler(handler http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Wrap the ResponseWriter to remember the status + rww := &ResponseWriterWrapper{-1, w} + begin := time.Now() + + // Common headers + origin := r.Header.Get("Origin") + rww.Header().Set("Access-Control-Allow-Origin", origin) + rww.Header().Set("Access-Control-Allow-Credentials", "true") + rww.Header().Set("Access-Control-Expose-Headers", "X-Server-Time") + rww.Header().Set("X-Server-Time", fmt.Sprintf("%v", begin.Unix())) + + defer func() { + // Send a 500 error if a panic happens during a handler. + // Without this, Chrome & Firefox were retrying aborted ajax requests, + // at least to my localhost. + if e := recover(); e != nil { + + // If RPCResponse + if res, ok := e.(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))) + } + } + + // Finally, log. + durationMS := time.Since(begin).Nanoseconds() / 1000000 + if rww.Status == -1 { + rww.Status = 200 + } + log.Info("Served RPC HTTP response", + "method", r.Method, "url", r.URL, + "status", rww.Status, "duration", durationMS, + "remoteAddr", r.RemoteAddr, + ) + }() + + handler.ServeHTTP(rww, r) + }) +} + +// Remember the status for logging +type ResponseWriterWrapper struct { + Status int + http.ResponseWriter +} + +func (w *ResponseWriterWrapper) WriteHeader(status int) { + w.Status = status + w.ResponseWriter.WriteHeader(status) +} + +// implements http.Hijacker +func (w *ResponseWriterWrapper) Hijack() (net.Conn, *bufio.ReadWriter, error) { + return w.ResponseWriter.(http.Hijacker).Hijack() +} + +// Stick it as a deferred statement in gouroutines to prevent the program from crashing. +func Recover(daemonName string) { + if e := recover(); e != nil { + stack := string(debug.Stack()) + errorString := fmt.Sprintf("[%s] %s\n%s", daemonName, e, stack) + alert.Alert(errorString) + } +} diff --git a/server/log.go b/server/log.go new file mode 100644 index 000000000..704e22e30 --- /dev/null +++ b/server/log.go @@ -0,0 +1,7 @@ +package rpcserver + +import ( + "github.com/tendermint/log15" +) + +var log = log15.New("module", "rpcserver") diff --git a/types/types.go b/types/types.go new file mode 100644 index 000000000..4905d000b --- /dev/null +++ b/types/types.go @@ -0,0 +1,71 @@ +package rpctypes + +import ( + "github.com/tendermint/go-events" +) + +type RPCRequest struct { + JSONRPC string `json:"jsonrpc"` + ID string `json:"id"` + Method string `json:"method"` + Params []interface{} `json:"params"` +} + +func NewRPCRequest(id string, method string, params []interface{}) RPCRequest { + return RPCRequest{ + JSONRPC: "2.0", + ID: id, + Method: method, + Params: params, + } +} + +//---------------------------------------- + +/* +Result is a generic interface. +Applications should register type-bytes like so: + +var _ = wire.RegisterInterface( + struct{ Result }{}, + wire.ConcreteType{&ResultGenesis{}, ResultTypeGenesis}, + wire.ConcreteType{&ResultBlockchainInfo{}, ResultTypeBlockchainInfo}, + ... +) +*/ +type Result interface { +} + +//---------------------------------------- + +type RPCResponse struct { + JSONRPC string `json:"jsonrpc"` + ID string `json:"id"` + Result Result `json:"result"` + Error string `json:"error"` +} + +func NewRPCResponse(id string, res Result, err string) RPCResponse { + return RPCResponse{ + JSONRPC: "2.0", + ID: id, + Result: res, + Error: err, + } +} + +//---------------------------------------- + +// *wsConnection implements this interface. +type WSRPCConnection interface { + GetRemoteAddr() string + GetEventSwitch() *events.EventSwitch + WriteRPCResponse(resp RPCResponse) + TryWriteRPCResponse(resp RPCResponse) bool +} + +// websocket-only RPCFuncs take this as the first parameter. +type WSRPCContext struct { + Request RPCRequest + WSRPCConnection +} diff --git a/version.go b/version.go new file mode 100644 index 000000000..2982824dd --- /dev/null +++ b/version.go @@ -0,0 +1,3 @@ +package rpc + +const Version = "0.4.0" From 0bcae125c20df24f5d592453d119ec0f4e0dc89c Mon Sep 17 00:00:00 2001 From: Ethan Buchman Date: Tue, 12 Jan 2016 18:29:31 -0500 Subject: [PATCH 03/73] use comma separated string for arg names --- client/ws_client.go | 3 --- server/handlers.go | 26 +++++++++++++++----------- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/client/ws_client.go b/client/ws_client.go index ae2b324a4..4ce1ca07e 100644 --- a/client/ws_client.go +++ b/client/ws_client.go @@ -54,12 +54,10 @@ func (wsc *WSClient) dial() error { // Set the ping/pong handlers con.SetPingHandler(func(m string) error { // NOTE: https://github.com/gorilla/websocket/issues/97 - log.Debug("Client received ping, writing pong") go con.WriteControl(websocket.PongMessage, []byte(m), time.Now().Add(time.Second*wsWriteTimeoutSeconds)) return nil }) con.SetPongHandler(func(m string) error { - log.Debug("Client received pong") // NOTE: https://github.com/gorilla/websocket/issues/97 return nil }) @@ -74,7 +72,6 @@ func (wsc *WSClient) OnStop() { func (wsc *WSClient) receiveEventsRoutine() { for { - log.Notice("Waiting for wsc message ...") _, data, err := wsc.ReadMessage() if err != nil { log.Info("WSClient failed to read message", "error", err, "data", string(data)) diff --git a/server/handlers.go b/server/handlers.go index 64c151808..843a7ada8 100644 --- a/server/handlers.go +++ b/server/handlers.go @@ -9,6 +9,7 @@ import ( "net/http" "reflect" "sort" + "strings" "time" "github.com/gorilla/websocket" @@ -41,23 +42,26 @@ type RPCFunc struct { } // wraps a function for quicker introspection -func NewRPCFunc(f interface{}, args []string) *RPCFunc { - return &RPCFunc{ - f: reflect.ValueOf(f), - args: funcArgTypes(f), - returns: funcReturnTypes(f), - argNames: args, - ws: false, - } +// f is the function, args are comma separated argument names +func NewRPCFunc(f interface{}, args string) *RPCFunc { + return newRPCFunc(f, args, false) +} + +func NewWSRPCFunc(f interface{}, args string) *RPCFunc { + return newRPCFunc(f, args, true) } -func NewWSRPCFunc(f interface{}, args []string) *RPCFunc { +func newRPCFunc(f interface{}, args string, ws bool) *RPCFunc { + var argNames []string + if args != "" { + argNames = strings.Split(args, ",") + } return &RPCFunc{ f: reflect.ValueOf(f), args: funcArgTypes(f), returns: funcReturnTypes(f), - argNames: args, - ws: true, + argNames: argNames, + ws: ws, } } From aff561d8c3a904d87ff9eb859b467772d8506e08 Mon Sep 17 00:00:00 2001 From: Ethan Buchman Date: Wed, 13 Jan 2016 18:37:35 -0500 Subject: [PATCH 04/73] RPCResponse.Result is json.RawMessage --- client/http_client.go | 25 ++++++++++++++----------- client/ws_client.go | 4 ++-- server/handlers.go | 18 +++++++++++------- server/http_server.go | 9 +++++++-- types/types.go | 16 ++++++++++------ 5 files changed, 44 insertions(+), 28 deletions(-) diff --git a/client/http_client.go b/client/http_client.go index 133f2b725..f614a776e 100644 --- a/client/http_client.go +++ b/client/http_client.go @@ -2,6 +2,7 @@ package rpcclient import ( "bytes" + "encoding/json" "errors" "io/ioutil" "net/http" @@ -22,8 +23,8 @@ func NewClientJSONRPC(remote string) *ClientJSONRPC { return &ClientJSONRPC{remote} } -func (c *ClientJSONRPC) Call(method string, params []interface{}) (interface{}, error) { - return CallHTTP_JSONRPC(c.remote, method, params) +func (c *ClientJSONRPC) Call(method string, params []interface{}, result interface{}) (interface{}, error) { + return CallHTTP_JSONRPC(c.remote, method, params, result) } // URI takes params as a map @@ -38,11 +39,11 @@ func NewClientURI(remote string) *ClientURI { return &ClientURI{remote} } -func (c *ClientURI) Call(method string, params map[string]interface{}) (interface{}, error) { - return CallHTTP_URI(c.remote, method, params) +func (c *ClientURI) Call(method string, params map[string]interface{}, result interface{}) (interface{}, error) { + return CallHTTP_URI(c.remote, method, params, result) } -func CallHTTP_JSONRPC(remote string, method string, params []interface{}) (interface{}, error) { +func CallHTTP_JSONRPC(remote string, method string, params []interface{}, result interface{}) (interface{}, error) { // Make request and get responseBytes request := rpctypes.RPCRequest{ JSONRPC: "2.0", @@ -63,10 +64,10 @@ func CallHTTP_JSONRPC(remote string, method string, params []interface{}) (inter return nil, err } log.Info(Fmt("RPC response: %v", string(responseBytes))) - return unmarshalResponseBytes(responseBytes) + return unmarshalResponseBytes(responseBytes, result) } -func CallHTTP_URI(remote string, method string, params map[string]interface{}) (interface{}, error) { +func CallHTTP_URI(remote string, method string, params map[string]interface{}, result interface{}) (interface{}, error) { values, err := argsToURLValues(params) if err != nil { return nil, err @@ -81,18 +82,18 @@ func CallHTTP_URI(remote string, method string, params map[string]interface{}) ( if err != nil { return nil, err } - return unmarshalResponseBytes(responseBytes) + return unmarshalResponseBytes(responseBytes, result) } //------------------------------------------------ -func unmarshalResponseBytes(responseBytes []byte) (interface{}, error) { +func unmarshalResponseBytes(responseBytes []byte, result interface{}) (interface{}, error) { // read response // if rpc/core/types is imported, the result will unmarshal // into the correct type var err error response := &rpctypes.RPCResponse{} - wire.ReadJSON(response, responseBytes, &err) + err = json.Unmarshal(responseBytes, response) if err != nil { return nil, err } @@ -100,7 +101,9 @@ func unmarshalResponseBytes(responseBytes []byte) (interface{}, error) { if errorStr != "" { return nil, errors.New(errorStr) } - return response.Result, err + // unmarshal the RawMessage into the result + result = wire.ReadJSONPtr(result, *response.Result, &err) + return result, err } func argsToURLValues(args map[string]interface{}) (url.Values, error) { diff --git a/client/ws_client.go b/client/ws_client.go index 4ce1ca07e..e0dc349ce 100644 --- a/client/ws_client.go +++ b/client/ws_client.go @@ -1,13 +1,13 @@ package rpcclient import ( + "encoding/json" "net/http" "time" "github.com/gorilla/websocket" . "github.com/tendermint/go-common" "github.com/tendermint/go-rpc/types" - "github.com/tendermint/go-wire" ) const ( @@ -79,7 +79,7 @@ func (wsc *WSClient) receiveEventsRoutine() { break } else { var response rpctypes.RPCResponse - wire.ReadJSON(&response, data, &err) + err := json.Unmarshal(data, &response) if err != nil { log.Info("WSClient failed to parse message", "error", err, "data", string(data)) wsc.Stop() diff --git a/server/handlers.go b/server/handlers.go index 843a7ada8..a463b21e1 100644 --- a/server/handlers.go +++ b/server/handlers.go @@ -19,6 +19,8 @@ import ( "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. +// "result" is the interface on which the result objects are registered, and is popualted with every RPCResponse func RegisterRPCFuncs(mux *http.ServeMux, funcMap map[string]*RPCFunc) { // HTTP endpoints for funcName, rpcFunc := range funcMap { @@ -433,7 +435,6 @@ func (wsc *wsConnection) readRoutine() { // receives on a write channel and writes out on the socket func (wsc *wsConnection) writeRoutine() { defer wsc.baseConn.Close() - var n, err = int(0), error(nil) for { select { case <-wsc.Quit: @@ -446,14 +447,12 @@ func (wsc *wsConnection) writeRoutine() { return } case msg := <-wsc.writeChan: - buf := new(bytes.Buffer) - wire.WriteJSON(msg, buf, &n, &err) + jsonBytes, err := json.Marshal(msg) if err != nil { log.Error("Failed to marshal RPCResponse to JSON", "error", err) } else { wsc.baseConn.SetWriteDeadline(time.Now().Add(time.Second * wsWriteTimeoutSeconds)) - bufBytes := buf.Bytes() - if err = wsc.baseConn.WriteMessage(websocket.TextMessage, bufBytes); err != nil { + if err = wsc.baseConn.WriteMessage(websocket.TextMessage, jsonBytes); err != nil { log.Warn("Failed to write response on websocket", "error", err) wsc.Stop() return @@ -507,13 +506,18 @@ func (wm *WebsocketManager) WebsocketHandler(w http.ResponseWriter, r *http.Requ // rpc.websocket //----------------------------------------------------------------------------- -// returns is result struct and error. If error is not nil, return it +// NOTE: assume returns is result struct and error. If error is not nil, return it func unreflectResult(returns []reflect.Value) (interface{}, error) { errV := returns[1] if errV.Interface() != nil { return nil, fmt.Errorf("%v", errV.Interface()) } - return returns[0].Interface(), nil + rv := returns[0] + // the result is a registered interface, + // we need a pointer to it so we can marshal with type byte + rvp := reflect.New(rv.Type()) + rvp.Elem().Set(rv) + return rvp.Interface(), nil } // writes a list of available rpc endpoints as an html page diff --git a/server/http_server.go b/server/http_server.go index 37b01ceee..1271d073b 100644 --- a/server/http_server.go +++ b/server/http_server.go @@ -3,6 +3,7 @@ package rpcserver import ( "bufio" + "encoding/json" "fmt" "net" "net/http" @@ -12,7 +13,7 @@ import ( "github.com/tendermint/go-alert" . "github.com/tendermint/go-common" . "github.com/tendermint/go-rpc/types" - "github.com/tendermint/go-wire" + //"github.com/tendermint/go-wire" ) func StartHTTPServer(listenAddr string, handler http.Handler) (net.Listener, error) { @@ -32,7 +33,11 @@ func StartHTTPServer(listenAddr string, handler http.Handler) (net.Listener, err } func WriteRPCResponseHTTP(w http.ResponseWriter, res RPCResponse) { - jsonBytes := wire.JSONBytesPretty(res) + // jsonBytes := wire.JSONBytesPretty(res) + jsonBytes, err := json.Marshal(res) + if err != nil { + panic(err) + } w.Header().Set("Content-Type", "application/json") w.WriteHeader(200) w.Write(jsonBytes) diff --git a/types/types.go b/types/types.go index 4905d000b..f9d1961c1 100644 --- a/types/types.go +++ b/types/types.go @@ -1,7 +1,10 @@ package rpctypes import ( + "encoding/json" + "github.com/tendermint/go-events" + "github.com/tendermint/go-wire" ) type RPCRequest struct { @@ -39,17 +42,18 @@ type Result interface { //---------------------------------------- type RPCResponse struct { - JSONRPC string `json:"jsonrpc"` - ID string `json:"id"` - Result Result `json:"result"` - Error string `json:"error"` + JSONRPC string `json:"jsonrpc"` + ID string `json:"id"` + Result *json.RawMessage `json:"result"` + Error string `json:"error"` } -func NewRPCResponse(id string, res Result, err string) RPCResponse { +func NewRPCResponse(id string, res interface{}, err string) RPCResponse { + raw := json.RawMessage(wire.JSONBytes(res)) return RPCResponse{ JSONRPC: "2.0", ID: id, - Result: res, + Result: &raw, Error: err, } } From 91c734d02edf03b689f84c22bc8e7530b8abbede Mon Sep 17 00:00:00 2001 From: Ethan Buchman Date: Wed, 13 Jan 2016 21:21:16 -0500 Subject: [PATCH 05/73] client: ResultsCh chan json.RawMessage, ErrorsCh --- client/ws_client.go | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/client/ws_client.go b/client/ws_client.go index e0dc349ce..a2506585b 100644 --- a/client/ws_client.go +++ b/client/ws_client.go @@ -2,6 +2,7 @@ package rpcclient import ( "encoding/json" + "fmt" "net/http" "time" @@ -12,6 +13,7 @@ import ( const ( wsResultsChannelCapacity = 10 + wsErrorsChannelCapacity = 1 wsWriteTimeoutSeconds = 10 ) @@ -19,7 +21,8 @@ type WSClient struct { QuitService Address string *websocket.Conn - ResultsCh chan rpctypes.Result // closes upon WSClient.Stop() + ResultsCh chan json.RawMessage // closes upon WSClient.Stop() + ErrorsCh chan error // closes upon WSClient.Stop() } // create a new connection @@ -27,7 +30,8 @@ func NewWSClient(addr string) *WSClient { wsClient := &WSClient{ Address: addr, Conn: nil, - ResultsCh: make(chan rpctypes.Result, wsResultsChannelCapacity), + ResultsCh: make(chan json.RawMessage, wsResultsChannelCapacity), + ErrorsCh: make(chan error, wsErrorsChannelCapacity), } wsClient.QuitService = *NewQuitService(log, "WSClient", wsClient) return wsClient @@ -67,7 +71,7 @@ func (wsc *WSClient) dial() error { func (wsc *WSClient) OnStop() { wsc.QuitService.OnStop() - // ResultsCh is closed in receiveEventsRoutine. + // ResultsCh/ErrorsCh is closed in receiveEventsRoutine. } func (wsc *WSClient) receiveEventsRoutine() { @@ -82,15 +86,20 @@ func (wsc *WSClient) receiveEventsRoutine() { err := json.Unmarshal(data, &response) if err != nil { log.Info("WSClient failed to parse message", "error", err, "data", string(data)) - wsc.Stop() - break + wsc.ErrorsCh <- err + continue } - wsc.ResultsCh <- response.Result + if response.Error != "" { + wsc.ErrorsCh <- fmt.Errorf(err.Error()) + continue + } + wsc.ResultsCh <- *response.Result } } // Cleanup close(wsc.ResultsCh) + close(wsc.ErrorsCh) } // subscribe to an event From 14735d5eb5401ac80b73a183d217d3638acf2d34 Mon Sep 17 00:00:00 2001 From: Ethan Buchman Date: Wed, 13 Jan 2016 22:16:56 -0500 Subject: [PATCH 06/73] RawMessage fix --- types/types.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/types/types.go b/types/types.go index f9d1961c1..a35e3dae8 100644 --- a/types/types.go +++ b/types/types.go @@ -49,11 +49,15 @@ type RPCResponse struct { } func NewRPCResponse(id string, res interface{}, err string) RPCResponse { - raw := json.RawMessage(wire.JSONBytes(res)) + var raw *json.RawMessage + if res != nil { + rawMsg := json.RawMessage(wire.JSONBytes(res)) + raw = &rawMsg + } return RPCResponse{ JSONRPC: "2.0", ID: id, - Result: &raw, + Result: raw, Error: err, } } From b9eec7e4380115aea4fd1c8962c4b399ac9afd4c Mon Sep 17 00:00:00 2001 From: Ethan Buchman Date: Wed, 20 Jan 2016 11:36:31 -0500 Subject: [PATCH 07/73] version bump --- version.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/version.go b/version.go index 2982824dd..d0a0ee9cd 100644 --- a/version.go +++ b/version.go @@ -1,3 +1,7 @@ package rpc -const Version = "0.4.0" +const Maj = "0" +const Min = "5" // refactored out of tendermint/tendermint; RPCResponse.Result is RawJSON +const Fix = "0" + +const Version = Maj + "." + Min + "." + Fix From fbc5ac80524e467d542ea9b4a55034255ee0691a Mon Sep 17 00:00:00 2001 From: Ethan Buchman Date: Thu, 21 Jan 2016 23:03:39 -0500 Subject: [PATCH 08/73] print method in client log --- client/http_client.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/http_client.go b/client/http_client.go index f614a776e..6bb746d2e 100644 --- a/client/http_client.go +++ b/client/http_client.go @@ -53,7 +53,7 @@ func CallHTTP_JSONRPC(remote string, method string, params []interface{}, result } requestBytes := wire.JSONBytes(request) requestBuf := bytes.NewBuffer(requestBytes) - log.Info(Fmt("RPC request to %v: %v", remote, string(requestBytes))) + log.Info(Fmt("RPC request to %v (%v): %v", remote, method, string(requestBytes))) httpResponse, err := http.Post(remote, "text/json", requestBuf) if err != nil { return nil, err @@ -72,7 +72,7 @@ func CallHTTP_URI(remote string, method string, params map[string]interface{}, r if err != nil { return nil, err } - log.Info(Fmt("URI request to %v: %v", remote, values)) + log.Info(Fmt("URI request to %v (%v): %v", remote, method, values)) resp, err := http.PostForm(remote+method, values) if err != nil { return nil, err From 45f57198cc54e5db21f66dc30726d0ef8fab1311 Mon Sep 17 00:00:00 2001 From: Ethan Buchman Date: Wed, 3 Feb 2016 02:01:28 -0500 Subject: [PATCH 09/73] client: wsc.String() --- client/ws_client.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/client/ws_client.go b/client/ws_client.go index a2506585b..f7a23fca8 100644 --- a/client/ws_client.go +++ b/client/ws_client.go @@ -37,6 +37,10 @@ func NewWSClient(addr string) *WSClient { return wsClient } +func (wsc *WSClient) String() string { + return wsc.Address +} + func (wsc *WSClient) OnStart() error { wsc.QuitService.OnStart() err := wsc.dial() From 8b7969d6eade8d2ed358f97dc940574b2cd6c2b9 Mon Sep 17 00:00:00 2001 From: Jae Kwon Date: Mon, 8 Feb 2016 00:57:37 -0800 Subject: [PATCH 10/73] Add comments about goroutine-safety --- server/handlers.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/server/handlers.go b/server/handlers.go index a463b21e1..b150c6076 100644 --- a/server/handlers.go +++ b/server/handlers.go @@ -347,6 +347,7 @@ func (wsc *wsConnection) GetEventSwitch() *events.EventSwitch { // Implements WSRPCConnection // Blocking write to writeChan until service stops. +// Goroutine-safe func (wsc *wsConnection) WriteRPCResponse(resp RPCResponse) { select { case <-wsc.Quit: @@ -357,6 +358,7 @@ func (wsc *wsConnection) WriteRPCResponse(resp RPCResponse) { // Implements WSRPCConnection // Nonblocking write. +// Goroutine-safe func (wsc *wsConnection) TryWriteRPCResponse(resp RPCResponse) bool { select { case <-wsc.Quit: From 1370f89864b10518e9753078dc2d7f769685e9a0 Mon Sep 17 00:00:00 2001 From: Jae Kwon Date: Mon, 8 Feb 2016 02:20:34 -0800 Subject: [PATCH 11/73] Fix bug in receiveEventsRoutine error handling --- client/ws_client.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ws_client.go b/client/ws_client.go index f7a23fca8..4c994bfcc 100644 --- a/client/ws_client.go +++ b/client/ws_client.go @@ -94,7 +94,7 @@ func (wsc *WSClient) receiveEventsRoutine() { continue } if response.Error != "" { - wsc.ErrorsCh <- fmt.Errorf(err.Error()) + wsc.ErrorsCh <- fmt.Errorf(response.Error) continue } wsc.ResultsCh <- *response.Result From 6607232a5daf1b474c4578836750e71943793c48 Mon Sep 17 00:00:00 2001 From: Ethan Buchman Date: Thu, 18 Feb 2016 21:07:49 +0000 Subject: [PATCH 12/73] add support for unix sockets --- client/http_client.go | 83 +++++++++++++++++++++--------- rpc_test.go | 114 ++++++++++++++++++++++++++++++++++++++++++ server/http_server.go | 9 ++-- types/types.go | 14 ++++++ 4 files changed, 194 insertions(+), 26 deletions(-) create mode 100644 rpc_test.go diff --git a/client/http_client.go b/client/http_client.go index 6bb746d2e..100507196 100644 --- a/client/http_client.go +++ b/client/http_client.go @@ -5,45 +5,63 @@ import ( "encoding/json" "errors" "io/ioutil" + "net" "net/http" "net/url" - "strings" . "github.com/tendermint/go-common" "github.com/tendermint/go-rpc/types" "github.com/tendermint/go-wire" ) -// JSON rpc takes params as a slice -type ClientJSONRPC struct { - remote string +// Set the net.Dial manually so we can do http over tcp or unix. +// Get/Post require a dummyDomain but it's over written by the Transport +var dummyDomain = "http://dummyDomain/" + +func unixDial(remote string) func(string, string) (net.Conn, error) { + return func(proto, addr string) (conn net.Conn, err error) { + return net.Dial("unix", remote) + } } -func NewClientJSONRPC(remote string) *ClientJSONRPC { - return &ClientJSONRPC{remote} +func tcpDial(remote string) func(string, string) (net.Conn, error) { + return func(proto, addr string) (conn net.Conn, err error) { + return net.Dial("tcp", remote) + } } -func (c *ClientJSONRPC) Call(method string, params []interface{}, result interface{}) (interface{}, error) { - return CallHTTP_JSONRPC(c.remote, method, params, result) +func socketTransport(remote string) *http.Transport { + if rpctypes.SocketType(remote) == "unix" { + return &http.Transport{ + Dial: unixDial(remote), + } + } else { + return &http.Transport{ + Dial: tcpDial(remote), + } + } } -// URI takes params as a map -type ClientURI struct { +//------------------------------------------------------------------------------------ + +// JSON rpc takes params as a slice +type ClientJSONRPC struct { remote string + client *http.Client } -func NewClientURI(remote string) *ClientURI { - if !strings.HasSuffix(remote, "/") { - remote = remote + "/" +func NewClientJSONRPC(remote string) *ClientJSONRPC { + return &ClientJSONRPC{ + remote: remote, + client: &http.Client{Transport: socketTransport(remote)}, } - return &ClientURI{remote} } -func (c *ClientURI) Call(method string, params map[string]interface{}, result interface{}) (interface{}, error) { - return CallHTTP_URI(c.remote, method, params, result) +func (c *ClientJSONRPC) Call(method string, params []interface{}, result interface{}) (interface{}, error) { + return c.call(method, params, result) } -func CallHTTP_JSONRPC(remote string, method string, params []interface{}, result interface{}) (interface{}, error) { +func (c *ClientJSONRPC) call(method string, params []interface{}, result interface{}) (interface{}, error) { // Make request and get responseBytes request := rpctypes.RPCRequest{ JSONRPC: "2.0", @@ -53,8 +71,8 @@ func CallHTTP_JSONRPC(remote string, method string, params []interface{}, result } requestBytes := wire.JSONBytes(request) requestBuf := bytes.NewBuffer(requestBytes) - log.Info(Fmt("RPC request to %v (%v): %v", remote, method, string(requestBytes))) - httpResponse, err := http.Post(remote, "text/json", requestBuf) + log.Info(Fmt("RPC request to %v (%v): %v", c.remote, method, string(requestBytes))) + httpResponse, err := c.client.Post(dummyDomain, "text/json", requestBuf) if err != nil { return nil, err } @@ -63,17 +81,36 @@ func CallHTTP_JSONRPC(remote string, method string, params []interface{}, result if err != nil { return nil, err } - log.Info(Fmt("RPC response: %v", string(responseBytes))) + // log.Info(Fmt("RPC response: %v", string(responseBytes))) return unmarshalResponseBytes(responseBytes, result) } -func CallHTTP_URI(remote string, method string, params map[string]interface{}, result interface{}) (interface{}, error) { +//------------------------------------------------------------- + +// URI takes params as a map +type ClientURI struct { + remote string + client *http.Client +} + +func NewClientURI(remote string) *ClientURI { + return &ClientURI{ + remote: remote, + client: &http.Client{Transport: socketTransport(remote)}, + } +} + +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 } - log.Info(Fmt("URI request to %v (%v): %v", remote, method, values)) - resp, err := http.PostForm(remote+method, values) + log.Info(Fmt("URI request to %v (%v): %v", c.remote, method, values)) + resp, err := c.client.PostForm(dummyDomain+method, values) if err != nil { return nil, err } diff --git a/rpc_test.go b/rpc_test.go new file mode 100644 index 000000000..f81bf1a16 --- /dev/null +++ b/rpc_test.go @@ -0,0 +1,114 @@ +package rpc + +import ( + "net/http" + "testing" + "time" + + "github.com/tendermint/go-rpc/client" + "github.com/tendermint/go-rpc/server" + "github.com/tendermint/go-wire" +) + +// Client and Server should work over tcp or unix sockets +var ( + tcpAddr = "0.0.0.0:46657" + unixAddr = "/tmp/go-rpc.sock" // NOTE: must remove file for test to run again +) + +// Define a type for results and register concrete versions +type Result interface{} + +type ResultStatus struct { + Value string +} + +var _ = wire.RegisterInterface( + struct{ Result }{}, + wire.ConcreteType{&ResultStatus{}, 0x1}, +) + +// Define some routes +var Routes = map[string]*rpcserver.RPCFunc{ + "status": rpcserver.NewRPCFunc(StatusResult, "arg"), +} + +// an rpc function +func StatusResult(v string) (Result, error) { + return &ResultStatus{v}, nil +} + +// launch unix and tcp servers +func init() { + mux := http.NewServeMux() + rpcserver.RegisterRPCFuncs(mux, Routes) + go func() { + _, err := rpcserver.StartHTTPServer(tcpAddr, mux) + if err != nil { + panic(err) + } + }() + + mux = http.NewServeMux() + rpcserver.RegisterRPCFuncs(mux, Routes) + go func() { + _, err := rpcserver.StartHTTPServer(unixAddr, mux) + if err != nil { + panic(err) + } + }() + + // wait for servers to start + time.Sleep(time.Second * 2) + +} + +func testURI(t *testing.T, cl *rpcclient.ClientURI) { + val := "acbd" + params := map[string]interface{}{ + "arg": val, + } + var result Result + _, err := cl.Call("status", params, &result) + if err != nil { + t.Fatal(err) + } + got := result.(*ResultStatus).Value + if got != val { + t.Fatalf("Got: %v .... Expected: %v \n", got, val) + } +} + +func testJSONRPC(t *testing.T, cl *rpcclient.ClientJSONRPC) { + val := "acbd" + params := []interface{}{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 TestURI_TCP(t *testing.T) { + cl := rpcclient.NewClientURI(tcpAddr) + testURI(t, cl) +} + +func TestURI_UNIX(t *testing.T) { + cl := rpcclient.NewClientURI(unixAddr) + testURI(t, cl) +} + +func TestJSONRPC_TCP(t *testing.T) { + cl := rpcclient.NewClientJSONRPC(tcpAddr) + testJSONRPC(t, cl) +} + +func TestJSONRPC_UNIX(t *testing.T) { + cl := rpcclient.NewClientJSONRPC(unixAddr) + testJSONRPC(t, cl) +} diff --git a/server/http_server.go b/server/http_server.go index 1271d073b..beec9bcc2 100644 --- a/server/http_server.go +++ b/server/http_server.go @@ -17,11 +17,14 @@ import ( ) func StartHTTPServer(listenAddr string, handler http.Handler) (net.Listener, error) { - log.Notice(Fmt("Starting RPC HTTP server on %v", listenAddr)) - listener, err := net.Listen("tcp", listenAddr) + // listenAddr is `IP:PORT` or /path/to/socket + socketType := SocketType(listenAddr) + log.Notice(Fmt("Starting RPC HTTP server on %s socket %v", socketType, listenAddr)) + listener, err := net.Listen(socketType, listenAddr) if err != nil { - return nil, fmt.Errorf("Failed to listen to %v", listenAddr) + return nil, fmt.Errorf("Failed to listen to %v: %v", listenAddr, err) } + go func() { res := http.Serve( listener, diff --git a/types/types.go b/types/types.go index a35e3dae8..d7461f4e1 100644 --- a/types/types.go +++ b/types/types.go @@ -2,6 +2,7 @@ package rpctypes import ( "encoding/json" + "strings" "github.com/tendermint/go-events" "github.com/tendermint/go-wire" @@ -77,3 +78,16 @@ type WSRPCContext struct { Request RPCRequest WSRPCConnection } + +//---------------------------------------- +// sockets +// +// Determine if its a unix or tcp socket. +// If tcp, must specify the port; `0.0.0.0` will return incorrectly as "unix" since there's no port +func SocketType(listenAddr string) string { + socketType := "unix" + if len(strings.Split(listenAddr, ":")) == 2 { + socketType = "tcp" + } + return socketType +} From 74130008f7d44bdf1294b833a94d5ca9bda20cd6 Mon Sep 17 00:00:00 2001 From: Ethan Buchman Date: Fri, 19 Feb 2016 00:20:20 +0000 Subject: [PATCH 13/73] deduplicate dialFunc --- client/http_client.go | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/client/http_client.go b/client/http_client.go index 100507196..d49001ed0 100644 --- a/client/http_client.go +++ b/client/http_client.go @@ -18,26 +18,21 @@ import ( // Get/Post require a dummyDomain but it's over written by the Transport var dummyDomain = "http://dummyDomain/" -func unixDial(remote string) func(string, string) (net.Conn, error) { +func dialFunc(sockType, remote string) func(string, string) (net.Conn, error) { return func(proto, addr string) (conn net.Conn, err error) { - return net.Dial("unix", remote) - } -} - -func tcpDial(remote string) func(string, string) (net.Conn, error) { - return func(proto, addr string) (conn net.Conn, err error) { - return net.Dial("tcp", remote) + return net.Dial(sockType, remote) } } +// remote is IP:PORT or /path/to/socket func socketTransport(remote string) *http.Transport { if rpctypes.SocketType(remote) == "unix" { return &http.Transport{ - Dial: unixDial(remote), + Dial: dialFunc("unix", remote), } } else { return &http.Transport{ - Dial: tcpDial(remote), + Dial: dialFunc("tcp", remote), } } } From 1410693eae5400a50efbbac3f23b9e3e94b7d6c8 Mon Sep 17 00:00:00 2001 From: Ethan Buchman Date: Fri, 19 Feb 2016 02:05:24 +0000 Subject: [PATCH 14/73] support unix domain websockets --- client/http_client.go | 18 +++++---------- client/ws_client.go | 16 ++++++++----- rpc_test.go | 52 +++++++++++++++++++++++++++++++++++++++++++ types/types.go | 2 +- 4 files changed, 70 insertions(+), 18 deletions(-) diff --git a/client/http_client.go b/client/http_client.go index d49001ed0..e9e18bfa8 100644 --- a/client/http_client.go +++ b/client/http_client.go @@ -16,24 +16,18 @@ import ( // Set the net.Dial manually so we can do http over tcp or unix. // Get/Post require a dummyDomain but it's over written by the Transport -var dummyDomain = "http://dummyDomain/" +var dummyDomain = "http://dummyDomain" -func dialFunc(sockType, remote string) func(string, string) (net.Conn, error) { +func dialer(remote string) func(string, string) (net.Conn, error) { return func(proto, addr string) (conn net.Conn, err error) { - return net.Dial(sockType, remote) + return net.Dial(rpctypes.SocketType(remote), remote) } } // remote is IP:PORT or /path/to/socket func socketTransport(remote string) *http.Transport { - if rpctypes.SocketType(remote) == "unix" { - return &http.Transport{ - Dial: dialFunc("unix", remote), - } - } else { - return &http.Transport{ - Dial: dialFunc("tcp", remote), - } + return &http.Transport{ + Dial: dialer(remote), } } @@ -105,7 +99,7 @@ func (c *ClientURI) call(method string, params map[string]interface{}, result in return nil, err } log.Info(Fmt("URI request to %v (%v): %v", c.remote, method, values)) - resp, err := c.client.PostForm(dummyDomain+method, values) + resp, err := c.client.PostForm(dummyDomain+"/"+method, values) if err != nil { return nil, err } diff --git a/client/ws_client.go b/client/ws_client.go index 4c994bfcc..a8b8ce80c 100644 --- a/client/ws_client.go +++ b/client/ws_client.go @@ -19,16 +19,18 @@ const ( type WSClient struct { QuitService - Address string + Address string // IP:PORT or /path/to/socket + Endpoint string // /websocket/url/endpoint *websocket.Conn ResultsCh chan json.RawMessage // closes upon WSClient.Stop() ErrorsCh chan error // closes upon WSClient.Stop() } // create a new connection -func NewWSClient(addr string) *WSClient { +func NewWSClient(addr, endpoint string) *WSClient { wsClient := &WSClient{ Address: addr, + Endpoint: endpoint, Conn: nil, ResultsCh: make(chan json.RawMessage, wsResultsChannelCapacity), ErrorsCh: make(chan error, wsErrorsChannelCapacity), @@ -38,7 +40,7 @@ func NewWSClient(addr string) *WSClient { } func (wsc *WSClient) String() string { - return wsc.Address + return wsc.Address + ", " + wsc.Endpoint } func (wsc *WSClient) OnStart() error { @@ -52,10 +54,14 @@ func (wsc *WSClient) OnStart() error { } func (wsc *WSClient) dial() error { + // Dial - dialer := websocket.DefaultDialer + dialer := &websocket.Dialer{ + NetDial: dialer(wsc.Address), + Proxy: http.ProxyFromEnvironment, + } rHeader := http.Header{} - con, _, err := dialer.Dial(wsc.Address, rHeader) + con, _, err := dialer.Dial("ws://"+dummyDomain+wsc.Endpoint, rHeader) if err != nil { return err } diff --git a/rpc_test.go b/rpc_test.go index f81bf1a16..f23f6cdfb 100644 --- a/rpc_test.go +++ b/rpc_test.go @@ -7,6 +7,7 @@ import ( "github.com/tendermint/go-rpc/client" "github.com/tendermint/go-rpc/server" + "github.com/tendermint/go-rpc/types" "github.com/tendermint/go-wire" ) @@ -14,6 +15,8 @@ import ( var ( tcpAddr = "0.0.0.0:46657" unixAddr = "/tmp/go-rpc.sock" // NOTE: must remove file for test to run again + + websocketEndpoint = "/websocket/endpoint" ) // Define a type for results and register concrete versions @@ -42,6 +45,8 @@ func StatusResult(v string) (Result, error) { func init() { mux := http.NewServeMux() rpcserver.RegisterRPCFuncs(mux, Routes) + wm := rpcserver.NewWebsocketManager(Routes, nil) + mux.HandleFunc(websocketEndpoint, wm.WebsocketHandler) go func() { _, err := rpcserver.StartHTTPServer(tcpAddr, mux) if err != nil { @@ -51,6 +56,8 @@ func init() { mux = http.NewServeMux() rpcserver.RegisterRPCFuncs(mux, Routes) + wm = rpcserver.NewWebsocketManager(Routes, nil) + mux.HandleFunc(websocketEndpoint, wm.WebsocketHandler) go func() { _, err := rpcserver.StartHTTPServer(unixAddr, mux) if err != nil { @@ -93,6 +100,33 @@ func testJSONRPC(t *testing.T, cl *rpcclient.ClientJSONRPC) { } } +func testWS(t *testing.T, cl *rpcclient.WSClient) { + val := "acbd" + params := []interface{}{val} + err := cl.WriteJSON(rpctypes.RPCRequest{ + JSONRPC: "2.0", + ID: "", + Method: "status", + Params: params, + }) + if err != nil { + t.Fatal(err) + } + + msg := <-cl.ResultsCh + result := new(Result) + wire.ReadJSONPtr(result, msg, &err) + if err != nil { + t.Fatal(err) + } + got := (*result).(*ResultStatus).Value + if got != val { + t.Fatalf("Got: %v .... Expected: %v \n", got, val) + } +} + +//------------- + func TestURI_TCP(t *testing.T) { cl := rpcclient.NewClientURI(tcpAddr) testURI(t, cl) @@ -112,3 +146,21 @@ func TestJSONRPC_UNIX(t *testing.T) { cl := rpcclient.NewClientJSONRPC(unixAddr) testJSONRPC(t, cl) } + +func TestWS_TCP(t *testing.T) { + cl := rpcclient.NewWSClient(tcpAddr, websocketEndpoint) + _, err := cl.Start() + if err != nil { + t.Fatal(err) + } + 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) +} diff --git a/types/types.go b/types/types.go index d7461f4e1..5d79c4a66 100644 --- a/types/types.go +++ b/types/types.go @@ -86,7 +86,7 @@ type WSRPCContext struct { // If tcp, must specify the port; `0.0.0.0` will return incorrectly as "unix" since there's no port func SocketType(listenAddr string) string { socketType := "unix" - if len(strings.Split(listenAddr, ":")) == 2 { + if len(strings.Split(listenAddr, ":")) >= 2 { socketType = "tcp" } return socketType From e8538d606aa9700da03f748f61d734e8cbc1438b Mon Sep 17 00:00:00 2001 From: Ethan Buchman Date: Wed, 4 May 2016 10:39:43 -0400 Subject: [PATCH 15/73] add blank client interface --- client/http_client.go | 7 ++++++- server/handlers.go | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/client/http_client.go b/client/http_client.go index e9e18bfa8..7c0480600 100644 --- a/client/http_client.go +++ b/client/http_client.go @@ -33,6 +33,11 @@ func socketTransport(remote string) *http.Transport { //------------------------------------------------------------------------------------ +type Client interface { +} + +//------------------------------------------------------------------------------------ + // JSON rpc takes params as a slice type ClientJSONRPC struct { remote string @@ -60,7 +65,7 @@ func (c *ClientJSONRPC) call(method string, params []interface{}, result interfa } requestBytes := wire.JSONBytes(request) 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(dummyDomain, "text/json", requestBuf) if err != nil { return nil, err diff --git a/server/handlers.go b/server/handlers.go index b150c6076..1c447c6c9 100644 --- a/server/handlers.go +++ b/server/handlers.go @@ -387,7 +387,7 @@ func (wsc *wsConnection) readRoutine() { // We use `readTimeout` to handle read timeouts. _, in, err := wsc.baseConn.ReadMessage() if err != nil { - log.Notice("Failed to read from connection", "remote", wsc.remoteAddr) + log.Notice("Failed to read from connection", "remote", wsc.remoteAddr, "err", err.Error()) // an error reading the connection, // kill the connection wsc.Stop() From 1d9e89812adc202811b7fb8e9e0837e73adadb43 Mon Sep 17 00:00:00 2001 From: Jae Kwon Date: Sun, 8 May 2016 14:58:15 -0700 Subject: [PATCH 16/73] Remove go-alert dependency --- server/http_server.go | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/server/http_server.go b/server/http_server.go index beec9bcc2..cc3da39be 100644 --- a/server/http_server.go +++ b/server/http_server.go @@ -10,7 +10,6 @@ import ( "runtime/debug" "time" - "github.com/tendermint/go-alert" . "github.com/tendermint/go-common" . "github.com/tendermint/go-rpc/types" //"github.com/tendermint/go-wire" @@ -112,12 +111,3 @@ func (w *ResponseWriterWrapper) WriteHeader(status int) { func (w *ResponseWriterWrapper) Hijack() (net.Conn, *bufio.ReadWriter, error) { return w.ResponseWriter.(http.Hijacker).Hijack() } - -// Stick it as a deferred statement in gouroutines to prevent the program from crashing. -func Recover(daemonName string) { - if e := recover(); e != nil { - stack := string(debug.Stack()) - errorString := fmt.Sprintf("[%s] %s\n%s", daemonName, e, stack) - alert.Alert(errorString) - } -} From 7ea86f65066b8a9bf448ef595d72929ed9999a9a Mon Sep 17 00:00:00 2001 From: Ethan Buchman Date: Tue, 21 Jun 2016 15:19:56 -0400 Subject: [PATCH 17/73] fix test race and update readme --- README.md | 57 +++++++++++++++++++++++++++++++++++++++++++++++++++++ rpc_test.go | 8 ++++---- 2 files changed, 61 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index e74cf8021..f6e725c23 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,61 @@ # go-rpc +[![CircleCI](https://circleci.com/gh/tendermint/go-rpc.svg?style=svg)](https://circleci.com/gh/tendermint/go-rpc) + HTTP RPC server supporting calls via uri params, jsonrpc, and jsonrpc over websockets +# How To + +Define some types and routes: + +``` +// Define a type for results and register concrete versions with go-wire +type Result interface{} + +type ResultStatus struct { + Value string +} + +var _ = wire.RegisterInterface( + struct{ Result }{}, + wire.ConcreteType{&ResultStatus{}, 0x1}, +) + +// Define some routes +var Routes = map[string]*rpcserver.RPCFunc{ + "status": rpcserver.NewRPCFunc(StatusResult, "arg"), +} + +// an rpc function +func StatusResult(v string) (Result, error) { + return &ResultStatus{v}, nil +} + +``` + +Now start the server: + +``` +mux := http.NewServeMux() +rpcserver.RegisterRPCFuncs(mux, Routes) +wm := rpcserver.NewWebsocketManager(Routes, nil) +mux.HandleFunc("/websocket", wm.WebsocketHandler) +go func() { + _, err := rpcserver.StartHTTPServer("0.0.0.0:46657", mux) + if err != nil { + panic(err) + } +}() + +``` + +Note that unix sockets are supported as well (eg. `/path/to/socket` instead of `0.0.0.0:46657`) + +Now see all available endpoints by sending a GET request to `0.0.0.0:46657`. +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) diff --git a/rpc_test.go b/rpc_test.go index f23f6cdfb..82bdeb136 100644 --- a/rpc_test.go +++ b/rpc_test.go @@ -54,12 +54,12 @@ func init() { } }() - mux = http.NewServeMux() - rpcserver.RegisterRPCFuncs(mux, Routes) + mux2 := http.NewServeMux() + rpcserver.RegisterRPCFuncs(mux2, Routes) wm = rpcserver.NewWebsocketManager(Routes, nil) - mux.HandleFunc(websocketEndpoint, wm.WebsocketHandler) + mux2.HandleFunc(websocketEndpoint, wm.WebsocketHandler) go func() { - _, err := rpcserver.StartHTTPServer(unixAddr, mux) + _, err := rpcserver.StartHTTPServer(unixAddr, mux2) if err != nil { panic(err) } From 0c70a4636acc77ffcfdcccc5e6e57305bd7183a2 Mon Sep 17 00:00:00 2001 From: Ethan Buchman Date: Thu, 23 Jun 2016 20:33:04 -0400 Subject: [PATCH 18/73] add better docs to readme --- README.md | 58 +++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 54 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index f6e725c23..a68823101 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,57 @@ HTTP RPC server supporting calls via uri params, jsonrpc, and jsonrpc over websockets -# How To +# Client Requests + +Suppose we want to expose the rpc function `HelloWorld(name string, num int)`. + +## GET (URI) + +As a GET request, it would have URI encoded parameters, and look like: + +``` +curl 'http://localhost:8008/hello_world?name="my_world"&num=5' +``` + +Note the `'` around the url, which is just so bash doesn't ignore the quotes in `"my_world"`. +This should also work: + +``` +curl http://localhost:8008/hello_world?name=\"my_world\"&num=5 +``` + +A GET request to `/` returns a list of available endpoints. +For those which take arguments, the arguments will be listed in order, with `_` where the actual value should be. + +## POST (JSONRPC) + +As a POST request, we use JSONRPC. For instance, the same request would have this as the body: + +``` +{ + "jsonrpc":"2.0", + "id":"anything", + "method":"hello_world", + "params":["my_world", 5] +} +``` + +Note the `params` does not currently support key-value pairs (#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 + +``` +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`, +though this is configurable when starting the server. + +# Server Definition Define some types and routes: @@ -41,7 +91,7 @@ rpcserver.RegisterRPCFuncs(mux, Routes) wm := rpcserver.NewWebsocketManager(Routes, nil) mux.HandleFunc("/websocket", wm.WebsocketHandler) go func() { - _, err := rpcserver.StartHTTPServer("0.0.0.0:46657", mux) + _, err := rpcserver.StartHTTPServer("0.0.0.0:8008", mux) if err != nil { panic(err) } @@ -49,9 +99,9 @@ go func() { ``` -Note that unix sockets are supported as well (eg. `/path/to/socket` instead of `0.0.0.0:46657`) +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:46657`. +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 From a44e0e0f4b1a3d18060dc97ffca9a11e83bb2c13 Mon Sep 17 00:00:00 2001 From: Ethan Buchman Date: Thu, 23 Jun 2016 20:36:59 -0400 Subject: [PATCH 19/73] add test example --- Makefile | 11 +++++++++++ circle.yml | 23 +++++++++++++++++++++++ test/data.json | 6 ++++++ test/main.go | 35 +++++++++++++++++++++++++++++++++++ test/test.sh | 23 +++++++++++++++++++++++ 5 files changed, 98 insertions(+) create mode 100644 Makefile create mode 100644 circle.yml create mode 100644 test/data.json create mode 100644 test/main.go create mode 100644 test/test.sh diff --git a/Makefile b/Makefile new file mode 100644 index 000000000..6d72cf6ff --- /dev/null +++ b/Makefile @@ -0,0 +1,11 @@ +.PHONY: all test get_deps + +all: test + +test: + go test github.com/tendermint/go-rpc/... + cd ./test && bash test.sh + + +get_deps: + go get -t -d github.com/tendermint/go-rpc/... diff --git a/circle.yml b/circle.yml new file mode 100644 index 000000000..f022d5a6b --- /dev/null +++ b/circle.yml @@ -0,0 +1,23 @@ +machine: + environment: + GOPATH: /home/ubuntu/.go_workspace + REPO: $GOPATH/src/github.com/$CIRCLE_PROJECT_USERNAME/$CIRCLE_PROJECT_REPONAME + hosts: + circlehost: 127.0.0.1 + localhost: 127.0.0.1 + +checkout: + post: + - 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 && make get_deps" + +test: + override: + - "cd $REPO && make test" diff --git a/test/data.json b/test/data.json new file mode 100644 index 000000000..eac2e0dfe --- /dev/null +++ b/test/data.json @@ -0,0 +1,6 @@ +{ + "jsonrpc":"2.0", + "id":"", + "method":"hello_world", + "params":["my_world", 5] +} diff --git a/test/main.go b/test/main.go new file mode 100644 index 000000000..d14ab05f5 --- /dev/null +++ b/test/main.go @@ -0,0 +1,35 @@ +package main + +import ( + "fmt" + "net/http" + + . "github.com/tendermint/go-common" + rpcserver "github.com/tendermint/go-rpc/server" +) + +var routes = map[string]*rpcserver.RPCFunc{ + "hello_world": rpcserver.NewRPCFunc(HelloWorld, "name,num"), +} + +func HelloWorld(name string, num int) (Result, error) { + return Result{fmt.Sprintf("hi %s %d", name, num)}, nil +} + +type Result struct { + Result string +} + +func main() { + mux := http.NewServeMux() + rpcserver.RegisterRPCFuncs(mux, routes) + _, err := rpcserver.StartHTTPServer("0.0.0.0:8008", mux) + if err != nil { + Exit(err.Error()) + } + + // Wait forever + TrapSignal(func() { + }) + +} diff --git a/test/test.sh b/test/test.sh new file mode 100644 index 000000000..3d1f599dd --- /dev/null +++ b/test/test.sh @@ -0,0 +1,23 @@ +#! /bin/bash +set -e + +go build -o server main.go +./server > /dev/null & +PID=$! +sleep 2 + + +R1=`curl -s 'http://localhost:8008/hello_world?name="my_world"&num=5'` + + +R2=`curl -s --data @data.json http://localhost:8008` + +kill -9 $PID + +if [[ "$R1" != "$R2" ]]; then + echo "responses are not identical:" + echo "R1: $R1" + echo "R2: $R2" + exit 1 +fi +echo "Success" From a8ac81913984df85d2a848e5fc89a6d87f349662 Mon Sep 17 00:00:00 2001 From: Ethan Buchman Date: Thu, 23 Jun 2016 20:40:16 -0400 Subject: [PATCH 20/73] link issue from readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a68823101..1a91fb693 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ As a POST request, we use JSONRPC. For instance, the same request would have thi } ``` -Note the `params` does not currently support key-value pairs (#1), so order matters (you can get the order from making a +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 From 39ee59c26e3b25aeef7c0e2c360f0fe1a27a4182 Mon Sep 17 00:00:00 2001 From: Ethan Buchman Date: Fri, 22 Jul 2016 01:13:16 -0400 Subject: [PATCH 21/73] server: return result with error --- server/handlers.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/handlers.go b/server/handlers.go index 1c447c6c9..82fd7b358 100644 --- a/server/handlers.go +++ b/server/handlers.go @@ -132,7 +132,7 @@ func makeJSONRPCHandler(funcMap map[string]*RPCFunc) http.HandlerFunc { log.Info("HTTPJSONRPC", "method", request.Method, "args", args, "returns", returns) result, err := unreflectResult(returns) if err != nil { - WriteRPCResponseHTTP(w, NewRPCResponse(request.ID, nil, err.Error())) + WriteRPCResponseHTTP(w, NewRPCResponse(request.ID, result, err.Error())) return } WriteRPCResponseHTTP(w, NewRPCResponse(request.ID, result, "")) From dea910cd3e71bbfaf1973fd7ba295f0ee515a25f Mon Sep 17 00:00:00 2001 From: Ethan Buchman Date: Fri, 22 Jul 2016 01:15:52 -0400 Subject: [PATCH 22/73] Makefile: go test --race --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 6d72cf6ff..4cc7c1594 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ all: test test: - go test github.com/tendermint/go-rpc/... + go test --race github.com/tendermint/go-rpc/... cd ./test && bash test.sh From 479510be0e80dd9e5d6b1f941adad168df0af85f Mon Sep 17 00:00:00 2001 From: Ethan Buchman Date: Wed, 10 Aug 2016 01:12:01 -0400 Subject: [PATCH 23/73] support full urls (with eg tcp:// prefix) --- client/http_client.go | 58 +++++++++++++++++++++++++++---------------- client/ws_client.go | 10 +++++--- rpc_test.go | 4 +-- server/http_server.go | 23 +++++++++++++---- version.go | 2 +- 5 files changed, 65 insertions(+), 32 deletions(-) diff --git a/client/http_client.go b/client/http_client.go index 7c0480600..63dd9a82d 100644 --- a/client/http_client.go +++ b/client/http_client.go @@ -8,26 +8,40 @@ import ( "net" "net/http" "net/url" + "strings" . "github.com/tendermint/go-common" "github.com/tendermint/go-rpc/types" "github.com/tendermint/go-wire" ) -// Set the net.Dial manually so we can do http over tcp or unix. -// Get/Post require a dummyDomain but it's over written by the Transport -var dummyDomain = "http://dummyDomain" +// TODO: Deprecate support for IP:PORT or /path/to/socket +func makeHTTPDialer(remoteAddr string) (string, func(string, string) (net.Conn, error)) { -func dialer(remote string) func(string, string) (net.Conn, error) { - return func(proto, addr string) (conn net.Conn, err error) { - return net.Dial(rpctypes.SocketType(remote), remote) + parts := strings.SplitN(remoteAddr, "://", 2) + 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) + address = remoteAddr + } else { + protocol, address = parts[0], parts[1] + } + + trimmedAddress := strings.Replace(address, "/", ".", -1) // replace / with . for http requests (dummy domain) + return trimmedAddress, func(proto, addr string) (net.Conn, error) { + return net.Dial(protocol, address) } } -// remote is IP:PORT or /path/to/socket -func socketTransport(remote string) *http.Transport { - return &http.Transport{ - Dial: dialer(remote), +// We overwrite the http.Client.Dial so we can do http over tcp or unix. +// remoteAddr should be fully featured (eg. with tcp:// or unix://) +func makeHTTPClient(remoteAddr string) (string, *http.Client) { + address, dialer := makeHTTPDialer(remoteAddr) + return "http://" + address, &http.Client{ + Transport: &http.Transport{ + Dial: dialer, + }, } } @@ -40,14 +54,15 @@ type Client interface { // JSON rpc takes params as a slice type ClientJSONRPC struct { - remote string - client *http.Client + address string + client *http.Client } func NewClientJSONRPC(remote string) *ClientJSONRPC { + address, client := makeHTTPClient(remote) return &ClientJSONRPC{ - remote: remote, - client: &http.Client{Transport: socketTransport(remote)}, + address: address, + client: client, } } @@ -66,7 +81,7 @@ func (c *ClientJSONRPC) call(method string, params []interface{}, result interfa requestBytes := wire.JSONBytes(request) requestBuf := bytes.NewBuffer(requestBytes) // log.Info(Fmt("RPC request to %v (%v): %v", c.remote, method, string(requestBytes))) - httpResponse, err := c.client.Post(dummyDomain, "text/json", requestBuf) + httpResponse, err := c.client.Post(c.address, "text/json", requestBuf) if err != nil { return nil, err } @@ -83,14 +98,15 @@ func (c *ClientJSONRPC) call(method string, params []interface{}, result interfa // URI takes params as a map type ClientURI struct { - remote string - client *http.Client + address string + client *http.Client } func NewClientURI(remote string) *ClientURI { + address, client := makeHTTPClient(remote) return &ClientURI{ - remote: remote, - client: &http.Client{Transport: socketTransport(remote)}, + address: address, + client: client, } } @@ -103,8 +119,8 @@ func (c *ClientURI) call(method string, params map[string]interface{}, result in if err != nil { return nil, err } - log.Info(Fmt("URI request to %v (%v): %v", c.remote, method, values)) - resp, err := c.client.PostForm(dummyDomain+"/"+method, values) + log.Info(Fmt("URI request to %v (%v): %v", c.address, method, values)) + resp, err := c.client.PostForm(c.address+"/"+method, values) if err != nil { return nil, err } diff --git a/client/ws_client.go b/client/ws_client.go index a8b8ce80c..00e4222ad 100644 --- a/client/ws_client.go +++ b/client/ws_client.go @@ -3,6 +3,7 @@ package rpcclient import ( "encoding/json" "fmt" + "net" "net/http" "time" @@ -21,15 +22,18 @@ type WSClient struct { QuitService Address string // IP:PORT or /path/to/socket Endpoint string // /websocket/url/endpoint + Dialer func(string, string) (net.Conn, error) *websocket.Conn ResultsCh chan json.RawMessage // closes upon WSClient.Stop() ErrorsCh chan error // closes upon WSClient.Stop() } // create a new connection -func NewWSClient(addr, endpoint string) *WSClient { +func NewWSClient(remoteAddr, endpoint string) *WSClient { + addr, dialer := makeHTTPDialer(remoteAddr) wsClient := &WSClient{ Address: addr, + Dialer: dialer, Endpoint: endpoint, Conn: nil, ResultsCh: make(chan json.RawMessage, wsResultsChannelCapacity), @@ -57,11 +61,11 @@ func (wsc *WSClient) dial() error { // Dial dialer := &websocket.Dialer{ - NetDial: dialer(wsc.Address), + NetDial: wsc.Dialer, Proxy: http.ProxyFromEnvironment, } rHeader := http.Header{} - con, _, err := dialer.Dial("ws://"+dummyDomain+wsc.Endpoint, rHeader) + con, _, err := dialer.Dial("ws://"+wsc.Address+wsc.Endpoint, rHeader) if err != nil { return err } diff --git a/rpc_test.go b/rpc_test.go index 82bdeb136..0c6e1dbdb 100644 --- a/rpc_test.go +++ b/rpc_test.go @@ -13,8 +13,8 @@ import ( // Client and Server should work over tcp or unix sockets var ( - tcpAddr = "0.0.0.0:46657" - unixAddr = "/tmp/go-rpc.sock" // NOTE: must remove file for test to run again + tcpAddr = "tcp://0.0.0.0:46657" + unixAddr = "unix:///tmp/go-rpc.sock" // NOTE: must remove file for test to run again websocketEndpoint = "/websocket/endpoint" ) diff --git a/server/http_server.go b/server/http_server.go index cc3da39be..26163cf12 100644 --- a/server/http_server.go +++ b/server/http_server.go @@ -8,6 +8,7 @@ import ( "net" "net/http" "runtime/debug" + "strings" "time" . "github.com/tendermint/go-common" @@ -15,11 +16,23 @@ import ( //"github.com/tendermint/go-wire" ) -func StartHTTPServer(listenAddr string, handler http.Handler) (net.Listener, error) { - // listenAddr is `IP:PORT` or /path/to/socket - socketType := SocketType(listenAddr) - log.Notice(Fmt("Starting RPC HTTP server on %s socket %v", socketType, listenAddr)) - listener, err := net.Listen(socketType, listenAddr) +func StartHTTPServer(listenAddr string, handler http.Handler) (listener net.Listener, err error) { + // listenAddr should be fully formed including tcp:// or unix:// prefix + var proto, addr string + parts := strings.SplitN(listenAddr, "://", 2) + if len(parts) != 2 { + 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) + addr = listenAddr + // return nil, fmt.Errorf("Invalid listener address %s", lisenAddr) + } else { + proto, addr = parts[0], parts[1] + } + + log.Notice(Fmt("Starting RPC HTTP server on %s socket %v", proto, addr)) + listener, err = net.Listen(proto, addr) if err != nil { return nil, fmt.Errorf("Failed to listen to %v: %v", listenAddr, err) } diff --git a/version.go b/version.go index d0a0ee9cd..edf8d40da 100644 --- a/version.go +++ b/version.go @@ -2,6 +2,6 @@ package rpc const Maj = "0" const Min = "5" // refactored out of tendermint/tendermint; RPCResponse.Result is RawJSON -const Fix = "0" +const Fix = "1" // support tcp:// or unix:// prefixes const Version = Maj + "." + Min + "." + Fix From 855255d73eecd25097288be70f3fb208a5817d80 Mon Sep 17 00:00:00 2001 From: Ethan Buchman Date: Mon, 10 Oct 2016 03:22:34 -0400 Subject: [PATCH 24/73] use EventSwitch interface; less logging --- client/http_client.go | 12 ++++++++---- server/handlers.go | 22 ++++++++++++---------- types/types.go | 2 +- 3 files changed, 21 insertions(+), 15 deletions(-) diff --git a/client/http_client.go b/client/http_client.go index 63dd9a82d..816791b5c 100644 --- a/client/http_client.go +++ b/client/http_client.go @@ -119,7 +119,7 @@ func (c *ClientURI) call(method string, params map[string]interface{}, result in if err != nil { return nil, err } - log.Info(Fmt("URI request to %v (%v): %v", c.address, method, values)) + //log.Info(Fmt("URI request to %v (%v): %v", c.address, method, values)) resp, err := c.client.PostForm(c.address+"/"+method, values) if err != nil { return nil, err @@ -138,19 +138,23 @@ func unmarshalResponseBytes(responseBytes []byte, result interface{}) (interface // read response // if rpc/core/types is imported, the result will unmarshal // into the correct type + // log.Notice("response", "response", string(responseBytes)) var err error response := &rpctypes.RPCResponse{} err = json.Unmarshal(responseBytes, response) if err != nil { - return nil, err + return nil, errors.New(Fmt("Error unmarshalling rpc response: %v", err)) } errorStr := response.Error if errorStr != "" { - return nil, errors.New(errorStr) + return nil, errors.New(Fmt("Response error: %v", errorStr)) } // unmarshal the RawMessage into the result result = wire.ReadJSONPtr(result, *response.Result, &err) - return result, err + if err != nil { + return nil, errors.New(Fmt("Error unmarshalling rpc response result: %v", err)) + } + return result, nil } func argsToURLValues(args map[string]interface{}) (url.Values, error) { diff --git a/server/handlers.go b/server/handlers.go index 82fd7b358..2b2bb90ce 100644 --- a/server/handlers.go +++ b/server/handlers.go @@ -107,7 +107,7 @@ func makeJSONRPCHandler(funcMap map[string]*RPCFunc) http.HandlerFunc { var request RPCRequest err := json.Unmarshal(b, &request) if err != nil { - WriteRPCResponseHTTP(w, NewRPCResponse("", nil, err.Error())) + WriteRPCResponseHTTP(w, NewRPCResponse("", nil, fmt.Sprintf("Error unmarshalling request: %v", err.Error()))) return } if len(r.URL.Path) > 1 { @@ -125,14 +125,14 @@ func makeJSONRPCHandler(funcMap map[string]*RPCFunc) http.HandlerFunc { } args, err := jsonParamsToArgs(rpcFunc, request.Params) if err != nil { - WriteRPCResponseHTTP(w, NewRPCResponse(request.ID, nil, err.Error())) + WriteRPCResponseHTTP(w, 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, err.Error())) + WriteRPCResponseHTTP(w, NewRPCResponse(request.ID, result, fmt.Sprintf("Error unreflecting result: %v", err.Error()))) return } WriteRPCResponseHTTP(w, NewRPCResponse(request.ID, result, "")) @@ -201,16 +201,17 @@ func makeHTTPHandler(rpcFunc *RPCFunc) func(http.ResponseWriter, *http.Request) } // All other endpoints return func(w http.ResponseWriter, r *http.Request) { + log.Debug("HTTP HANDLER", "req", r) args, err := httpParamsToArgs(rpcFunc, r) if err != nil { - WriteRPCResponseHTTP(w, NewRPCResponse("", nil, err.Error())) + WriteRPCResponseHTTP(w, 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, err.Error())) + WriteRPCResponseHTTP(w, NewRPCResponse("", nil, fmt.Sprintf("Error unreflecting result: %v", err.Error()))) return } WriteRPCResponseHTTP(w, NewRPCResponse("", result, "")) @@ -228,6 +229,7 @@ func httpParamsToArgs(rpcFunc *RPCFunc, r *http.Request) ([]reflect.Value, error for i, name := range argNames { ty := argTypes[i] arg := GetParam(r, name) + //log.Notice("param to arg", "ty", ty, "name", name, "arg", arg) values[i], err = _jsonStringToArg(ty, arg) if err != nil { return nil, err @@ -271,11 +273,11 @@ type wsConnection struct { pingTicker *time.Ticker funcMap map[string]*RPCFunc - evsw *events.EventSwitch + evsw events.EventSwitch } // new websocket connection wrapper -func NewWSConnection(baseConn *websocket.Conn, funcMap map[string]*RPCFunc, evsw *events.EventSwitch) *wsConnection { +func NewWSConnection(baseConn *websocket.Conn, funcMap map[string]*RPCFunc, evsw events.EventSwitch) *wsConnection { wsc := &wsConnection{ remoteAddr: baseConn.RemoteAddr().String(), baseConn: baseConn, @@ -341,7 +343,7 @@ func (wsc *wsConnection) GetRemoteAddr() string { } // Implements WSRPCConnection -func (wsc *wsConnection) GetEventSwitch() *events.EventSwitch { +func (wsc *wsConnection) GetEventSwitch() events.EventSwitch { return wsc.evsw } @@ -472,10 +474,10 @@ func (wsc *wsConnection) writeRoutine() { type WebsocketManager struct { websocket.Upgrader funcMap map[string]*RPCFunc - evsw *events.EventSwitch + evsw events.EventSwitch } -func NewWebsocketManager(funcMap map[string]*RPCFunc, evsw *events.EventSwitch) *WebsocketManager { +func NewWebsocketManager(funcMap map[string]*RPCFunc, evsw events.EventSwitch) *WebsocketManager { return &WebsocketManager{ funcMap: funcMap, evsw: evsw, diff --git a/types/types.go b/types/types.go index 5d79c4a66..ee4a63cc8 100644 --- a/types/types.go +++ b/types/types.go @@ -68,7 +68,7 @@ func NewRPCResponse(id string, res interface{}, err string) RPCResponse { // *wsConnection implements this interface. type WSRPCConnection interface { GetRemoteAddr() string - GetEventSwitch() *events.EventSwitch + GetEventSwitch() events.EventSwitch WriteRPCResponse(resp RPCResponse) TryWriteRPCResponse(resp RPCResponse) bool } From 161e36fd56c2f95ad133dd03ddb33db0363ca742 Mon Sep 17 00:00:00 2001 From: Jae Kwon Date: Fri, 28 Oct 2016 12:04:58 -0700 Subject: [PATCH 25/73] QuitService->BaseService --- client/ws_client.go | 8 ++++---- server/handlers.go | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/client/ws_client.go b/client/ws_client.go index 00e4222ad..4d975f8ec 100644 --- a/client/ws_client.go +++ b/client/ws_client.go @@ -19,7 +19,7 @@ const ( ) type WSClient struct { - QuitService + 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.QuitService = *NewQuitService(log, "WSClient", wsClient) + wsClient.BaseService = *NewBaseService(log, "WSClient", wsClient) return wsClient } @@ -48,7 +48,7 @@ func (wsc *WSClient) String() string { } func (wsc *WSClient) OnStart() error { - wsc.QuitService.OnStart() + wsc.BaseService.OnStart() err := wsc.dial() if err != nil { return err @@ -84,7 +84,7 @@ func (wsc *WSClient) dial() error { } func (wsc *WSClient) OnStop() { - wsc.QuitService.OnStop() + wsc.BaseService.OnStop() // ResultsCh/ErrorsCh is closed in receiveEventsRoutine. } diff --git a/server/handlers.go b/server/handlers.go index 2b2bb90ce..dbc5c6e75 100644 --- a/server/handlers.go +++ b/server/handlers.go @@ -264,7 +264,7 @@ const ( // contains listener id, underlying ws connection, // and the event switch for subscribing to events type wsConnection struct { - QuitService + BaseService remoteAddr string baseConn *websocket.Conn @@ -285,13 +285,13 @@ func NewWSConnection(baseConn *websocket.Conn, funcMap map[string]*RPCFunc, evsw funcMap: funcMap, evsw: evsw, } - wsc.QuitService = *NewQuitService(log, "wsConnection", wsc) + wsc.BaseService = *NewBaseService(log, "wsConnection", wsc) return wsc } // wsc.Start() blocks until the connection closes. func (wsc *wsConnection) OnStart() error { - wsc.QuitService.OnStart() + wsc.BaseService.OnStart() // Read subscriptions/unsubscriptions to events go wsc.readRoutine() @@ -318,7 +318,7 @@ func (wsc *wsConnection) OnStart() error { } func (wsc *wsConnection) OnStop() { - wsc.QuitService.OnStop() + wsc.BaseService.OnStop() wsc.evsw.RemoveListener(wsc.remoteAddr) wsc.readTimeout.Stop() wsc.pingTicker.Stop() From 34a806578ac0dd578c5595365a9c1478e0a689a0 Mon Sep 17 00:00:00 2001 From: Matt Bell Date: Mon, 2 Jan 2017 09:50:20 -0800 Subject: [PATCH 26/73] Handle hex strings and quoted strings in HTTP params Use 0x-prefixed hex strings in client server: Decode hex string args Encode all string args as 0x without trying to encode as JSON Added tests for special string arguments Fix server handling quoted string args Added string arg handling test cases to bash test script --- client/http_client.go | 19 ++++++++++++++++++- rpc_test.go | 36 ++++++++++++++++++++++++++++++++++++ server/handlers.go | 31 ++++++++++++++++++++++++++++++- test/test.sh | 31 +++++++++++++++++++++++++------ 4 files changed, 109 insertions(+), 8 deletions(-) mode change 100644 => 100755 test/test.sh diff --git a/client/http_client.go b/client/http_client.go index 816791b5c..54fbd1c2b 100644 --- a/client/http_client.go +++ b/client/http_client.go @@ -4,10 +4,12 @@ import ( "bytes" "encoding/json" "errors" + "fmt" "io/ioutil" "net" "net/http" "net/url" + "reflect" "strings" . "github.com/tendermint/go-common" @@ -119,7 +121,7 @@ func (c *ClientURI) call(method string, params map[string]interface{}, result in if err != nil { return nil, err } - //log.Info(Fmt("URI request to %v (%v): %v", c.address, method, values)) + log.Info(Fmt("URI request to %v (%v): %v", c.address, method, values)) resp, err := c.client.PostForm(c.address+"/"+method, values) if err != nil { return nil, err @@ -176,6 +178,21 @@ func argsToJson(args map[string]interface{}) error { var n int var err error for k, v := range args { + // Convert strings to "0x"-prefixed hex + str, isString := reflect.ValueOf(v).Interface().(string) + if isString { + args[k] = fmt.Sprintf("0x%X", str) + continue + } + + // Convert byte slices to "0x"-prefixed hex + byteSlice, isByteSlice := reflect.ValueOf(v).Interface().([]byte) + if isByteSlice { + args[k] = fmt.Sprintf("0x%X", byteSlice) + continue + } + + // Pass everything else to go-wire buf := new(bytes.Buffer) wire.WriteJSON(v, buf, &n, &err) if err != nil { diff --git a/rpc_test.go b/rpc_test.go index 0c6e1dbdb..1fcda0e5e 100644 --- a/rpc_test.go +++ b/rpc_test.go @@ -164,3 +164,39 @@ func TestWS_UNIX(t *testing.T) { } testWS(t, cl) } + +func TestHexStringArg(t *testing.T) { + cl := rpcclient.NewClientURI(tcpAddr) + // should NOT be handled as hex + val := "0xabc" + params := map[string]interface{}{ + "arg": val, + } + var result Result + _, err := cl.Call("status", params, &result) + if err != nil { + t.Fatal(err) + } + got := result.(*ResultStatus).Value + if got != val { + t.Fatalf("Got: %v .... Expected: %v \n", got, val) + } +} + +func TestQuotedStringArg(t *testing.T) { + cl := rpcclient.NewClientURI(tcpAddr) + // should NOT be unquoted + val := "\"abc\"" + params := map[string]interface{}{ + "arg": val, + } + var result Result + _, err := cl.Call("status", params, &result) + if err != nil { + t.Fatal(err) + } + got := result.(*ResultStatus).Value + if got != val { + t.Fatalf("Got: %v .... Expected: %v \n", got, val) + } +} diff --git a/server/handlers.go b/server/handlers.go index dbc5c6e75..f5730213b 100644 --- a/server/handlers.go +++ b/server/handlers.go @@ -2,6 +2,7 @@ package rpcserver import ( "bytes" + "encoding/hex" "encoding/json" "errors" "fmt" @@ -229,7 +230,35 @@ func httpParamsToArgs(rpcFunc *RPCFunc, r *http.Request) ([]reflect.Value, error 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", "ty", ty, "name", name, "arg", arg) + + // Handle quoted strings + if strings.HasPrefix(arg, "\"") && strings.HasSuffix(arg, "\"") { + data := arg[1 : len(arg)-1] + if ty.Kind() == reflect.String { + values[i] = reflect.ValueOf(string(data)) + } else { + values[i] = reflect.ValueOf(data) + } + continue + } + + // Handle hex strings + if strings.HasPrefix(strings.ToLower(arg), "0x") { + var value []byte + value, err = hex.DecodeString(arg[2:]) + if err != nil { + return nil, err + } + if ty.Kind() == reflect.String { + values[i] = reflect.ValueOf(string(value)) + } else { + values[i] = reflect.ValueOf(value) + } + continue + } + + // Pass values to go-wire values[i], err = _jsonStringToArg(ty, arg) if err != nil { return nil, err diff --git a/test/test.sh b/test/test.sh old mode 100644 new mode 100755 index 3d1f599dd..a5f8abd44 --- a/test/test.sh +++ b/test/test.sh @@ -6,18 +6,37 @@ go build -o server main.go PID=$! sleep 2 - +# simple JSONRPC 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" +else + echo "Success" +fi -kill -9 $PID +# 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" +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 -echo "Success" + +kill -9 $PID From af1212897cfb1b2c43c02508e85babcf88fb5343 Mon Sep 17 00:00:00 2001 From: Matt Bell Date: Sat, 7 Jan 2017 14:00:27 -0800 Subject: [PATCH 27/73] Exit early in bash tests --- test/test.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/test.sh b/test/test.sh index a5f8abd44..2db809948 100755 --- a/test/test.sh +++ b/test/test.sh @@ -13,6 +13,7 @@ if [[ "$R1" != "$R2" ]]; then echo "responses are not identical:" echo "R1: $R1" echo "R2: $R2" + exit 1 else echo "Success" fi @@ -24,6 +25,7 @@ if [[ "$R1" != "$R2" ]]; then echo "responses are not identical:" echo "R1: $R1" echo "R2: $R2" + exit 1 else echo "Success" fi @@ -35,6 +37,7 @@ if [[ "$R1" != "$R2" ]]; then echo "responses are not identical:" echo "R1: $R1" echo "R2: $R2" + exit 1 else echo "Success" fi From 86506cd4f83f67c307e5f32ad8ca39a337158fe8 Mon Sep 17 00:00:00 2001 From: Matt Bell Date: Sat, 7 Jan 2017 14:21:10 -0800 Subject: [PATCH 28/73] Handle quoted and hex string type HTTP args for both 'string' and '[]byte' type function args --- client/http_client.go | 9 +----- server/handlers.go | 66 ++++++++++++++++++++++++++++--------------- 2 files changed, 44 insertions(+), 31 deletions(-) diff --git a/client/http_client.go b/client/http_client.go index 54fbd1c2b..57da5d6ec 100644 --- a/client/http_client.go +++ b/client/http_client.go @@ -121,7 +121,7 @@ func (c *ClientURI) call(method string, params map[string]interface{}, result in if err != nil { return nil, err } - log.Info(Fmt("URI request to %v (%v): %v", c.address, method, values)) + // log.Info(Fmt("URI request to %v (%v): %v", c.address, method, values)) resp, err := c.client.PostForm(c.address+"/"+method, values) if err != nil { return nil, err @@ -178,13 +178,6 @@ func argsToJson(args map[string]interface{}) error { var n int var err error for k, v := range args { - // Convert strings to "0x"-prefixed hex - str, isString := reflect.ValueOf(v).Interface().(string) - if isString { - args[k] = fmt.Sprintf("0x%X", str) - continue - } - // Convert byte slices to "0x"-prefixed hex byteSlice, isByteSlice := reflect.ValueOf(v).Interface().([]byte) if isByteSlice { diff --git a/server/handlers.go b/server/handlers.go index f5730213b..7a4ec1a45 100644 --- a/server/handlers.go +++ b/server/handlers.go @@ -225,36 +225,18 @@ func httpParamsToArgs(rpcFunc *RPCFunc, r *http.Request) ([]reflect.Value, error argTypes := rpcFunc.args argNames := rpcFunc.argNames - var err error 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) - // Handle quoted strings - if strings.HasPrefix(arg, "\"") && strings.HasSuffix(arg, "\"") { - data := arg[1 : len(arg)-1] - if ty.Kind() == reflect.String { - values[i] = reflect.ValueOf(string(data)) - } else { - values[i] = reflect.ValueOf(data) - } - continue + v, err, ok := nonJsonToArg(ty, arg) + if err != nil { + return nil, err } - - // Handle hex strings - if strings.HasPrefix(strings.ToLower(arg), "0x") { - var value []byte - value, err = hex.DecodeString(arg[2:]) - if err != nil { - return nil, err - } - if ty.Kind() == reflect.String { - values[i] = reflect.ValueOf(string(value)) - } else { - values[i] = reflect.ValueOf(value) - } + if ok { + values[i] = v continue } @@ -278,6 +260,44 @@ func _jsonStringToArg(ty reflect.Type, arg string) (reflect.Value, error) { return v, nil } +func nonJsonToArg(ty reflect.Type, arg string) (reflect.Value, error, bool) { + isQuotedString := strings.HasPrefix(arg, `"`) && strings.HasSuffix(arg, `"`) + isHexString := strings.HasPrefix(strings.ToLower(arg), "0x") + expectingString := ty.Kind() == reflect.String + expectingByteSlice := ty.Kind() == reflect.Slice && ty.Elem().Kind() == reflect.Uint8 + + if isHexString { + if !expectingString && !expectingByteSlice { + err := fmt.Errorf("Got a hex string arg, but expected '%s'", + ty.Kind().String()) + return reflect.ValueOf(nil), err, false + } + + var value []byte + value, err := hex.DecodeString(arg[2:]) + if err != nil { + return reflect.ValueOf(nil), err, false + } + if ty.Kind() == reflect.String { + return reflect.ValueOf(string(value)), nil, true + } + return reflect.ValueOf([]byte(value)), nil, true + } + + if isQuotedString && expectingByteSlice { + var err error + v := reflect.New(reflect.TypeOf("")) + wire.ReadJSONPtr(v.Interface(), []byte(arg), &err) + if err != nil { + return reflect.ValueOf(nil), err, false + } + v = v.Elem() + return reflect.ValueOf([]byte(v.String())), nil, true + } + + return reflect.ValueOf(nil), nil, false +} + // rpc.http //----------------------------------------------------------------------------- // rpc.websocket From 4d7aa62a10068450f977ebe786ea1412bf633658 Mon Sep 17 00:00:00 2001 From: Matt Bell Date: Sat, 7 Jan 2017 14:21:49 -0800 Subject: [PATCH 29/73] Added test for unexpected hex string type HTTP args --- test/test.sh | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/test/test.sh b/test/test.sh index 2db809948..dc22ad2f3 100755 --- a/test/test.sh +++ b/test/test.sh @@ -6,7 +6,7 @@ go build -o server main.go PID=$! sleep 2 -# simple JSONRPC request +# 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 @@ -42,4 +42,16 @@ else echo "Success" fi -kill -9 $PID +# 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 From 0eb278ad3b6ca0a1fcb7f012c02360bfb77da8ee Mon Sep 17 00:00:00 2001 From: Ethan Buchman Date: Thu, 12 Jan 2017 00:13:20 -0500 Subject: [PATCH 30/73] version bump 0.6.0 --- version.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.go b/version.go index edf8d40da..33eb7fe51 100644 --- a/version.go +++ b/version.go @@ -1,7 +1,7 @@ package rpc const Maj = "0" -const Min = "5" // refactored out of tendermint/tendermint; RPCResponse.Result is RawJSON -const Fix = "1" // support tcp:// or unix:// prefixes +const Min = "6" // 0x-prefixed string args handled as hex +const Fix = "0" // const Version = Maj + "." + Min + "." + Fix From 94fed25975c31e5d405369f0e3558da3cff85c2b Mon Sep 17 00:00:00 2001 From: Ethan Buchman Date: Thu, 12 Jan 2017 10:22:23 -0500 Subject: [PATCH 31/73] fix test --- test/test.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test.sh b/test/test.sh index dc22ad2f3..c38059016 100755 --- a/test/test.sh +++ b/test/test.sh @@ -44,7 +44,7 @@ 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'\"}" +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" From 08f2b5bc84f6c7b8151bd19a1d90da98207ea805 Mon Sep 17 00:00:00 2001 From: Ethan Buchman Date: Thu, 12 Jan 2017 21:46:50 -0500 Subject: [PATCH 32/73] get deps for testing --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 4cc7c1594..e73849878 100644 --- a/Makefile +++ b/Makefile @@ -2,10 +2,10 @@ all: test -test: +test: get_deps go test --race github.com/tendermint/go-rpc/... cd ./test && bash test.sh get_deps: - go get -t -d github.com/tendermint/go-rpc/... + go get -t -u github.com/tendermint/go-rpc/... From ac443fa61f30bbc65d7abfe71e897caaa9338bbc Mon Sep 17 00:00:00 2001 From: Ethan Buchman Date: Thu, 12 Jan 2017 21:59:02 -0500 Subject: [PATCH 33/73] run tests from bash script --- Makefile | 6 ++---- circle.yml | 4 ---- test/test.sh | 12 ++++++++++++ 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/Makefile b/Makefile index e73849878..3b005ea36 100644 --- a/Makefile +++ b/Makefile @@ -2,10 +2,8 @@ all: test -test: get_deps - go test --race github.com/tendermint/go-rpc/... - cd ./test && bash test.sh - +test: + bash ./test/test.sh get_deps: go get -t -u github.com/tendermint/go-rpc/... diff --git a/circle.yml b/circle.yml index f022d5a6b..1b3c8c609 100644 --- a/circle.yml +++ b/circle.yml @@ -14,10 +14,6 @@ checkout: # - git submodule sync # - git submodule update --init # use submodules -dependencies: - override: - - "cd $REPO && make get_deps" - test: override: - "cd $REPO && make test" diff --git a/test/test.sh b/test/test.sh index c38059016..f5e740241 100755 --- a/test/test.sh +++ b/test/test.sh @@ -1,4 +1,16 @@ #! /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 From 6177eb8398ebd4613fbecb71fd96d7c7d97303ec Mon Sep 17 00:00:00 2001 From: Ethan Buchman Date: Thu, 12 Jan 2017 22:01:20 -0500 Subject: [PATCH 34/73] love you circley --- circle.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/circle.yml b/circle.yml index 1b3c8c609..99af678c6 100644 --- a/circle.yml +++ b/circle.yml @@ -14,6 +14,10 @@ checkout: # - git submodule sync # - git submodule update --init # use submodules +dependencies: + override: + - "cd $REPO" + test: override: - "cd $REPO && make test" From b03facd828da4fe8ec325a57a89d72510308eeb7 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Tue, 7 Mar 2017 18:34:13 +0400 Subject: [PATCH 35/73] add Dockerfile --- Dockerfile | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 Dockerfile diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..c23c911ce --- /dev/null +++ b/Dockerfile @@ -0,0 +1,12 @@ +FROM golang:latest + +RUN mkdir -p /go/src/github.com/tendermint/go-rpc +WORKDIR /go/src/github.com/tendermint/go-rpc + +COPY Makefile /go/src/github.com/tendermint/go-rpc/ +# COPY glide.yaml /go/src/github.com/tendermint/go-rpc/ +# COPY glide.lock /go/src/github.com/tendermint/go-rpc/ + +COPY . /go/src/github.com/tendermint/go-rpc + +RUN make get_deps From e1d5873bdfc95728971a38297ec3150a556397f9 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Tue, 7 Mar 2017 18:34:54 +0400 Subject: [PATCH 36/73] support key-value params in JSONRPC (Refs #1) More changes: - remove Client interface (reason: empty) - introduce HTTPClient interface, which can be used for both ClientURI and ClientJSONRPC clients (so our users don't have to create their own) (Refs #8) - rename integration tests script to `integration_test.sh` - do not update deps on `get_deps` --- Makefile | 14 +++- README.md | 20 ++--- client/http_client.go | 32 ++++---- client/ws_client.go | 12 +-- rpc_test.go | 16 ++-- server/handlers.go | 103 ++++++++++++++++---------- test/data.json | 11 ++- test/{test.sh => integration_test.sh} | 31 ++++---- test/main.go | 6 +- types/types.go | 14 ++-- 10 files changed, 147 insertions(+), 112 deletions(-) rename test/{test.sh => integration_test.sh} (65%) diff --git a/Makefile b/Makefile index 3b005ea36..e17fa06ac 100644 --- a/Makefile +++ b/Makefile @@ -1,9 +1,15 @@ -.PHONY: all test get_deps +PACKAGES=$(shell go list ./...) all: test -test: - bash ./test/test.sh +test: + @echo "--> Running go test --race" + @go test --race $(PACKAGES) + @echo "--> Running integration tests" + @bash ./test/integration_test.sh get_deps: - go get -t -u github.com/tendermint/go-rpc/... + @echo "--> Running go get" + @go get -v -d $(PACKAGES) + +.PHONY: all test get_deps diff --git a/README.md b/README.md index 1a91fb693..4da0e5634 100644 --- a/README.md +++ b/README.md @@ -32,16 +32,16 @@ As a POST request, we use JSONRPC. For instance, the same request would have thi ``` { - "jsonrpc":"2.0", - "id":"anything", - "method":"hello_world", - "params":["my_world", 5] + "jsonrpc": "2.0", + "id": "anything", + "method": "hello_world", + "params": { + "name": "my_world", + "num": 5 + } } ``` -Note the `params` does not currently support key-value pairs (https://github.com/tendermint/go-rpc/issues/1), so order matters (you can get the order from making a -GET request to `/`) - With the above saved in file `data.json`, we can make the request with ``` @@ -50,8 +50,8 @@ curl --data @data.json http://localhost:8008 ## WebSocket (JSONRPC) -All requests are exposed over websocket in the same form as the POST JSONRPC. -Websocket connections are available at their own endpoint, typically `/websocket`, +All requests are exposed over websocket in the same form as the POST JSONRPC. +Websocket connections are available at their own endpoint, typically `/websocket`, though this is configurable when starting the server. # Server Definition @@ -102,7 +102,7 @@ go func() { Note that unix sockets are supported as well (eg. `/path/to/socket` instead of `0.0.0.0:8008`) Now see all available endpoints by sending a GET request to `0.0.0.0:8008`. -Each route is available as a GET request, as a JSONRPCv2 POST request, and via JSONRPCv2 over websockets +Each route is available as a GET request, as a JSONRPCv2 POST request, and via JSONRPCv2 over websockets. # Examples diff --git a/client/http_client.go b/client/http_client.go index 57da5d6ec..0c8bb6363 100644 --- a/client/http_client.go +++ b/client/http_client.go @@ -3,7 +3,6 @@ package rpcclient import ( "bytes" "encoding/json" - "errors" "fmt" "io/ioutil" "net" @@ -12,11 +11,16 @@ import ( "reflect" "strings" - . "github.com/tendermint/go-common" - "github.com/tendermint/go-rpc/types" - "github.com/tendermint/go-wire" + // cmn "github.com/tendermint/go-common" + rpctypes "github.com/tendermint/go-rpc/types" + wire "github.com/tendermint/go-wire" ) +// HTTPClient is a common interface for ClientJSONRPC and ClientURI. +type HTTPClient interface { + Call(method string, params []interface{}, result interface{}) (interface{}, error) +} + // TODO: Deprecate support for IP:PORT or /path/to/socket func makeHTTPDialer(remoteAddr string) (string, func(string, string) (net.Conn, error)) { @@ -49,11 +53,6 @@ func makeHTTPClient(remoteAddr string) (string, *http.Client) { //------------------------------------------------------------------------------------ -type Client interface { -} - -//------------------------------------------------------------------------------------ - // JSON rpc takes params as a slice type ClientJSONRPC struct { address string @@ -68,11 +67,11 @@ func NewClientJSONRPC(remote string) *ClientJSONRPC { } } -func (c *ClientJSONRPC) Call(method string, params []interface{}, result interface{}) (interface{}, error) { +func (c *ClientJSONRPC) Call(method string, params map[string]interface{}, result interface{}) (interface{}, error) { return c.call(method, params, result) } -func (c *ClientJSONRPC) call(method string, params []interface{}, result interface{}) (interface{}, error) { +func (c *ClientJSONRPC) call(method string, params map[string]interface{}, result interface{}) (interface{}, error) { // Make request and get responseBytes request := rpctypes.RPCRequest{ JSONRPC: "2.0", @@ -80,7 +79,10 @@ func (c *ClientJSONRPC) call(method string, params []interface{}, result interfa Params: params, ID: "", } - requestBytes := wire.JSONBytes(request) + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } requestBuf := bytes.NewBuffer(requestBytes) // log.Info(Fmt("RPC request to %v (%v): %v", c.remote, method, string(requestBytes))) httpResponse, err := c.client.Post(c.address, "text/json", requestBuf) @@ -145,16 +147,16 @@ func unmarshalResponseBytes(responseBytes []byte, result interface{}) (interface response := &rpctypes.RPCResponse{} err = json.Unmarshal(responseBytes, response) if err != nil { - return nil, errors.New(Fmt("Error unmarshalling rpc response: %v", err)) + return nil, fmt.Errorf("Error unmarshalling rpc response: %v", err) } errorStr := response.Error if errorStr != "" { - return nil, errors.New(Fmt("Response error: %v", errorStr)) + return nil, fmt.Errorf("Response error: %v", errorStr) } // unmarshal the RawMessage into the result result = wire.ReadJSONPtr(result, *response.Result, &err) if err != nil { - return nil, errors.New(Fmt("Error unmarshalling rpc response result: %v", err)) + return nil, fmt.Errorf("Error unmarshalling rpc response result: %v", err) } return result, nil } diff --git a/client/ws_client.go b/client/ws_client.go index 4d975f8ec..e5135d0af 100644 --- a/client/ws_client.go +++ b/client/ws_client.go @@ -8,8 +8,8 @@ import ( "time" "github.com/gorilla/websocket" - . "github.com/tendermint/go-common" - "github.com/tendermint/go-rpc/types" + cmn "github.com/tendermint/go-common" + rpctypes "github.com/tendermint/go-rpc/types" ) const ( @@ -19,7 +19,7 @@ const ( ) type WSClient struct { - BaseService + cmn.BaseService Address string // IP:PORT or /path/to/socket Endpoint string // /websocket/url/endpoint Dialer func(string, string) (net.Conn, error) @@ -39,7 +39,7 @@ func NewWSClient(remoteAddr, endpoint string) *WSClient { ResultsCh: make(chan json.RawMessage, wsResultsChannelCapacity), ErrorsCh: make(chan error, wsErrorsChannelCapacity), } - wsClient.BaseService = *NewBaseService(log, "WSClient", wsClient) + wsClient.BaseService = *cmn.NewBaseService(log, "WSClient", wsClient) return wsClient } @@ -122,7 +122,7 @@ func (wsc *WSClient) Subscribe(eventid string) error { JSONRPC: "2.0", ID: "", Method: "subscribe", - Params: []interface{}{eventid}, + Params: map[string]interface{}{"event": eventid}, }) return err } @@ -133,7 +133,7 @@ func (wsc *WSClient) Unsubscribe(eventid string) error { JSONRPC: "2.0", ID: "", Method: "unsubscribe", - Params: []interface{}{eventid}, + Params: map[string]interface{}{"event": eventid}, }) return err } diff --git a/rpc_test.go b/rpc_test.go index 1fcda0e5e..2c45c4c13 100644 --- a/rpc_test.go +++ b/rpc_test.go @@ -5,10 +5,10 @@ import ( "testing" "time" - "github.com/tendermint/go-rpc/client" - "github.com/tendermint/go-rpc/server" - "github.com/tendermint/go-rpc/types" - "github.com/tendermint/go-wire" + rpcclient "github.com/tendermint/go-rpc/client" + rpcserver "github.com/tendermint/go-rpc/server" + rpctypes "github.com/tendermint/go-rpc/types" + wire "github.com/tendermint/go-wire" ) // Client and Server should work over tcp or unix sockets @@ -88,7 +88,9 @@ func testURI(t *testing.T, cl *rpcclient.ClientURI) { func testJSONRPC(t *testing.T, cl *rpcclient.ClientJSONRPC) { val := "acbd" - params := []interface{}{val} + params := map[string]interface{}{ + "arg": val, + } var result Result _, err := cl.Call("status", params, &result) if err != nil { @@ -102,7 +104,9 @@ func testJSONRPC(t *testing.T, cl *rpcclient.ClientJSONRPC) { func testWS(t *testing.T, cl *rpcclient.WSClient) { val := "acbd" - params := []interface{}{val} + params := map[string]interface{}{ + "arg": val, + } err := cl.WriteJSON(rpctypes.RPCRequest{ JSONRPC: "2.0", ID: "", diff --git a/server/handlers.go b/server/handlers.go index 7a4ec1a45..beb664d9f 100644 --- a/server/handlers.go +++ b/server/handlers.go @@ -4,7 +4,6 @@ import ( "bytes" "encoding/hex" "encoding/json" - "errors" "fmt" "io/ioutil" "net/http" @@ -14,10 +13,10 @@ import ( "time" "github.com/gorilla/websocket" - . "github.com/tendermint/go-common" - "github.com/tendermint/go-events" - . "github.com/tendermint/go-rpc/types" - "github.com/tendermint/go-wire" + cmn "github.com/tendermint/go-common" + events "github.com/tendermint/go-events" + types "github.com/tendermint/go-rpc/types" + wire "github.com/tendermint/go-wire" ) // Adds a route for each function in the funcMap, as well as general jsonrpc and websocket handlers for all functions. @@ -105,75 +104,99 @@ func makeJSONRPCHandler(funcMap map[string]*RPCFunc) http.HandlerFunc { return } - var request RPCRequest + var request types.RPCRequest err := json.Unmarshal(b, &request) if err != nil { - WriteRPCResponseHTTP(w, NewRPCResponse("", nil, fmt.Sprintf("Error unmarshalling request: %v", err.Error()))) + WriteRPCResponseHTTP(w, types.NewRPCResponse("", nil, fmt.Sprintf("Error unmarshalling request: %v", err.Error()))) return } if len(r.URL.Path) > 1 { - WriteRPCResponseHTTP(w, NewRPCResponse(request.ID, nil, fmt.Sprintf("Invalid JSONRPC endpoint %s", r.URL.Path))) + WriteRPCResponseHTTP(w, types.NewRPCResponse(request.ID, nil, fmt.Sprintf("Invalid JSONRPC endpoint %s", r.URL.Path))) return } rpcFunc := funcMap[request.Method] if rpcFunc == nil { - WriteRPCResponseHTTP(w, NewRPCResponse(request.ID, nil, "RPC method unknown: "+request.Method)) + WriteRPCResponseHTTP(w, types.NewRPCResponse(request.ID, nil, "RPC method unknown: "+request.Method)) return } if rpcFunc.ws { - WriteRPCResponseHTTP(w, NewRPCResponse(request.ID, nil, "RPC method is only for websockets: "+request.Method)) + WriteRPCResponseHTTP(w, types.NewRPCResponse(request.ID, nil, "RPC method is only for websockets: "+request.Method)) return } args, err := jsonParamsToArgs(rpcFunc, request.Params) if err != nil { - WriteRPCResponseHTTP(w, NewRPCResponse(request.ID, nil, fmt.Sprintf("Error converting json params to arguments: %v", err.Error()))) + WriteRPCResponseHTTP(w, types.NewRPCResponse(request.ID, nil, fmt.Sprintf("Error converting json params to arguments: %v", err.Error()))) return } returns := rpcFunc.f.Call(args) log.Info("HTTPJSONRPC", "method", request.Method, "args", args, "returns", returns) result, err := unreflectResult(returns) if err != nil { - WriteRPCResponseHTTP(w, NewRPCResponse(request.ID, result, fmt.Sprintf("Error unreflecting result: %v", err.Error()))) + WriteRPCResponseHTTP(w, types.NewRPCResponse(request.ID, result, fmt.Sprintf("Error unreflecting result: %v", err.Error()))) return } - WriteRPCResponseHTTP(w, NewRPCResponse(request.ID, result, "")) + WriteRPCResponseHTTP(w, types.NewRPCResponse(request.ID, result, "")) } } // Convert a list of interfaces to properly typed values -func jsonParamsToArgs(rpcFunc *RPCFunc, params []interface{}) ([]reflect.Value, error) { +func jsonParamsToArgs(rpcFunc *RPCFunc, params map[string]interface{}) ([]reflect.Value, error) { if len(rpcFunc.argNames) != len(params) { - return nil, errors.New(fmt.Sprintf("Expected %v parameters (%v), got %v (%v)", - len(rpcFunc.argNames), rpcFunc.argNames, len(params), params)) + return nil, fmt.Errorf("Expected %v parameters (%v), got %v (%v)", + len(rpcFunc.argNames), rpcFunc.argNames, len(params), params) } + values := make([]reflect.Value, len(params)) - for i, p := range params { + + for name, param := range params { + i := indexOf(name, rpcFunc.argNames) + if -1 == i { + return nil, fmt.Errorf("%s is not an argument (args: %v)", name, rpcFunc.argNames) + } ty := rpcFunc.args[i] - v, err := _jsonObjectToArg(ty, p) + v, err := _jsonObjectToArg(ty, param) if err != nil { return nil, err } values[i] = v } + return values, nil } +// indexOf returns index of a string in a slice of strings, -1 if not found. +func indexOf(value string, values []string) int { + for i, v := range values { + if v == value { + return i + } + } + return -1 +} + // Same as above, but with the first param the websocket connection -func jsonParamsToArgsWS(rpcFunc *RPCFunc, params []interface{}, wsCtx WSRPCContext) ([]reflect.Value, error) { +func jsonParamsToArgsWS(rpcFunc *RPCFunc, params map[string]interface{}, wsCtx types.WSRPCContext) ([]reflect.Value, error) { if len(rpcFunc.argNames) != len(params) { - return nil, errors.New(fmt.Sprintf("Expected %v parameters (%v), got %v (%v)", - len(rpcFunc.argNames)-1, rpcFunc.argNames[1:], len(params), params)) + return nil, fmt.Errorf("Expected %v parameters (%v), got %v (%v)", + len(rpcFunc.argNames)-1, rpcFunc.argNames[1:], len(params), params) } + values := make([]reflect.Value, len(params)+1) values[0] = reflect.ValueOf(wsCtx) - for i, p := range params { + + for name, param := range params { + i := indexOf(name, rpcFunc.argNames) + if -1 == i { + return nil, fmt.Errorf("%s is not an argument (args: %v)", name, rpcFunc.argNames) + } ty := rpcFunc.args[i+1] - v, err := _jsonObjectToArg(ty, p) + v, err := _jsonObjectToArg(ty, param) if err != nil { return nil, err } values[i+1] = v } + return values, nil } @@ -197,7 +220,7 @@ func makeHTTPHandler(rpcFunc *RPCFunc) func(http.ResponseWriter, *http.Request) // Exception for websocket endpoints if rpcFunc.ws { return func(w http.ResponseWriter, r *http.Request) { - WriteRPCResponseHTTP(w, NewRPCResponse("", nil, "This RPC method is only for websockets")) + WriteRPCResponseHTTP(w, types.NewRPCResponse("", nil, "This RPC method is only for websockets")) } } // All other endpoints @@ -205,17 +228,17 @@ func makeHTTPHandler(rpcFunc *RPCFunc) func(http.ResponseWriter, *http.Request) log.Debug("HTTP HANDLER", "req", r) args, err := httpParamsToArgs(rpcFunc, r) if err != nil { - WriteRPCResponseHTTP(w, NewRPCResponse("", nil, fmt.Sprintf("Error converting http params to args: %v", err.Error()))) + WriteRPCResponseHTTP(w, types.NewRPCResponse("", nil, fmt.Sprintf("Error converting http params to args: %v", err.Error()))) return } returns := rpcFunc.f.Call(args) log.Info("HTTPRestRPC", "method", r.URL.Path, "args", args, "returns", returns) result, err := unreflectResult(returns) if err != nil { - WriteRPCResponseHTTP(w, NewRPCResponse("", nil, fmt.Sprintf("Error unreflecting result: %v", err.Error()))) + WriteRPCResponseHTTP(w, types.NewRPCResponse("", nil, fmt.Sprintf("Error unreflecting result: %v", err.Error()))) return } - WriteRPCResponseHTTP(w, NewRPCResponse("", result, "")) + WriteRPCResponseHTTP(w, types.NewRPCResponse("", result, "")) } } @@ -313,11 +336,11 @@ const ( // contains listener id, underlying ws connection, // and the event switch for subscribing to events type wsConnection struct { - BaseService + cmn.BaseService remoteAddr string baseConn *websocket.Conn - writeChan chan RPCResponse + writeChan chan types.RPCResponse readTimeout *time.Timer pingTicker *time.Ticker @@ -330,11 +353,11 @@ func NewWSConnection(baseConn *websocket.Conn, funcMap map[string]*RPCFunc, evsw wsc := &wsConnection{ remoteAddr: baseConn.RemoteAddr().String(), baseConn: baseConn, - writeChan: make(chan RPCResponse, writeChanCapacity), // error when full. + writeChan: make(chan types.RPCResponse, writeChanCapacity), // error when full. funcMap: funcMap, evsw: evsw, } - wsc.BaseService = *NewBaseService(log, "wsConnection", wsc) + wsc.BaseService = *cmn.NewBaseService(log, "wsConnection", wsc) return wsc } @@ -399,7 +422,7 @@ func (wsc *wsConnection) GetEventSwitch() events.EventSwitch { // Implements WSRPCConnection // Blocking write to writeChan until service stops. // Goroutine-safe -func (wsc *wsConnection) WriteRPCResponse(resp RPCResponse) { +func (wsc *wsConnection) WriteRPCResponse(resp types.RPCResponse) { select { case <-wsc.Quit: return @@ -410,7 +433,7 @@ func (wsc *wsConnection) WriteRPCResponse(resp RPCResponse) { // Implements WSRPCConnection // Nonblocking write. // Goroutine-safe -func (wsc *wsConnection) TryWriteRPCResponse(resp RPCResponse) bool { +func (wsc *wsConnection) TryWriteRPCResponse(resp types.RPCResponse) bool { select { case <-wsc.Quit: return false @@ -444,11 +467,11 @@ func (wsc *wsConnection) readRoutine() { wsc.Stop() return } - var request RPCRequest + var request types.RPCRequest err = json.Unmarshal(in, &request) if err != nil { errStr := fmt.Sprintf("Error unmarshaling data: %s", err.Error()) - wsc.WriteRPCResponse(NewRPCResponse(request.ID, nil, errStr)) + wsc.WriteRPCResponse(types.NewRPCResponse(request.ID, nil, errStr)) continue } @@ -456,28 +479,28 @@ func (wsc *wsConnection) readRoutine() { rpcFunc := wsc.funcMap[request.Method] if rpcFunc == nil { - wsc.WriteRPCResponse(NewRPCResponse(request.ID, nil, "RPC method unknown: "+request.Method)) + wsc.WriteRPCResponse(types.NewRPCResponse(request.ID, nil, "RPC method unknown: "+request.Method)) continue } var args []reflect.Value if rpcFunc.ws { - wsCtx := WSRPCContext{Request: request, WSRPCConnection: wsc} + wsCtx := types.WSRPCContext{Request: request, WSRPCConnection: wsc} args, err = jsonParamsToArgsWS(rpcFunc, request.Params, wsCtx) } else { args, err = jsonParamsToArgs(rpcFunc, request.Params) } if err != nil { - wsc.WriteRPCResponse(NewRPCResponse(request.ID, nil, err.Error())) + wsc.WriteRPCResponse(types.NewRPCResponse(request.ID, nil, err.Error())) continue } returns := rpcFunc.f.Call(args) log.Info("WSJSONRPC", "method", request.Method, "args", args, "returns", returns) result, err := unreflectResult(returns) if err != nil { - wsc.WriteRPCResponse(NewRPCResponse(request.ID, nil, err.Error())) + wsc.WriteRPCResponse(types.NewRPCResponse(request.ID, nil, err.Error())) continue } else { - wsc.WriteRPCResponse(NewRPCResponse(request.ID, result, "")) + wsc.WriteRPCResponse(types.NewRPCResponse(request.ID, result, "")) continue } diff --git a/test/data.json b/test/data.json index eac2e0dfe..83283ec33 100644 --- a/test/data.json +++ b/test/data.json @@ -1,6 +1,9 @@ { - "jsonrpc":"2.0", - "id":"", - "method":"hello_world", - "params":["my_world", 5] + "jsonrpc": "2.0", + "id": "", + "method": "hello_world", + "params": { + "name": "my_world", + "num": 5 + } } diff --git a/test/test.sh b/test/integration_test.sh similarity index 65% rename from test/test.sh rename to test/integration_test.sh index f5e740241..739708068 100755 --- a/test/test.sh +++ b/test/integration_test.sh @@ -1,17 +1,13 @@ -#! /bin/bash - -cd $GOPATH/src/github.com/tendermint/go-rpc - -# get deps -go get -u -t ./... - -# go tests -go test --race github.com/tendermint/go-rpc/... +#!/usr/bin/env bash +set -e +# Get the directory of where this script is. +SOURCE="${BASH_SOURCE[0]}" +while [ -h "$SOURCE" ] ; do SOURCE="$(readlink "$SOURCE")"; done +DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )" -# integration tests -cd test -set -e +# Change into that dir because we expect that. +pushd "$DIR" go build -o server main.go ./server > /dev/null & @@ -19,8 +15,8 @@ PID=$! sleep 2 # simple request -R1=`curl -s 'http://localhost:8008/hello_world?name="my_world"&num=5'` -R2=`curl -s --data @data.json http://localhost:8008` +R1=$(curl -s 'http://localhost:8008/hello_world?name="my_world"&num=5') +R2=$(curl -s --data @data.json http://localhost:8008) if [[ "$R1" != "$R2" ]]; then echo "responses are not identical:" echo "R1: $R1" @@ -31,7 +27,7 @@ else fi # request with 0x-prefixed hex string arg -R1=`curl -s 'http://localhost:8008/hello_world?name=0x41424344&num=123'` +R1=$(curl -s 'http://localhost:8008/hello_world?name=0x41424344&num=123') R2='{"jsonrpc":"2.0","id":"","result":{"Result":"hi ABCD 123"},"error":""}' if [[ "$R1" != "$R2" ]]; then echo "responses are not identical:" @@ -43,7 +39,7 @@ else fi # request with unquoted string arg -R1=`curl -s 'http://localhost:8008/hello_world?name=abcd&num=123'` +R1=$(curl -s 'http://localhost:8008/hello_world?name=abcd&num=123') R2="{\"jsonrpc\":\"2.0\",\"id\":\"\",\"result\":null,\"error\":\"Error converting http params to args: invalid character 'a' looking for beginning of value\"}" if [[ "$R1" != "$R2" ]]; then echo "responses are not identical:" @@ -55,7 +51,7 @@ else fi # request with string type when expecting number arg -R1=`curl -s 'http://localhost:8008/hello_world?name="abcd"&num=0xabcd'` +R1=$(curl -s 'http://localhost:8008/hello_world?name="abcd"&num=0xabcd') R2="{\"jsonrpc\":\"2.0\",\"id\":\"\",\"result\":null,\"error\":\"Error converting http params to args: Got a hex string arg, but expected 'int'\"}" if [[ "$R1" != "$R2" ]]; then echo "responses are not identical:" @@ -67,3 +63,4 @@ else fi kill -9 $PID || exit 0 +popd diff --git a/test/main.go b/test/main.go index d14ab05f5..28de2be88 100644 --- a/test/main.go +++ b/test/main.go @@ -4,7 +4,7 @@ import ( "fmt" "net/http" - . "github.com/tendermint/go-common" + cmn "github.com/tendermint/go-common" rpcserver "github.com/tendermint/go-rpc/server" ) @@ -25,11 +25,11 @@ func main() { rpcserver.RegisterRPCFuncs(mux, routes) _, err := rpcserver.StartHTTPServer("0.0.0.0:8008", mux) if err != nil { - Exit(err.Error()) + cmn.Exit(err.Error()) } // Wait forever - TrapSignal(func() { + cmn.TrapSignal(func() { }) } diff --git a/types/types.go b/types/types.go index ee4a63cc8..cebd7564a 100644 --- a/types/types.go +++ b/types/types.go @@ -4,18 +4,18 @@ import ( "encoding/json" "strings" - "github.com/tendermint/go-events" - "github.com/tendermint/go-wire" + events "github.com/tendermint/go-events" + wire "github.com/tendermint/go-wire" ) type RPCRequest struct { - JSONRPC string `json:"jsonrpc"` - ID string `json:"id"` - Method string `json:"method"` - Params []interface{} `json:"params"` + JSONRPC string `json:"jsonrpc"` + ID string `json:"id"` + Method string `json:"method"` + Params map[string]interface{} `json:"params"` } -func NewRPCRequest(id string, method string, params []interface{}) RPCRequest { +func NewRPCRequest(id string, method string, params map[string]interface{}) RPCRequest { return RPCRequest{ JSONRPC: "2.0", ID: id, From 66867bf94984dced859e24075dd5b462f3f09b8b Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Tue, 7 Mar 2017 19:04:24 +0400 Subject: [PATCH 37/73] remove "rpc" prefix from package imports --- client/http_client.go | 9 ++++---- client/ws_client.go | 8 +++---- rpc_test.go | 49 ++++++++++++++++++++++--------------------- server/http_server.go | 14 ++++++------- 4 files changed, 39 insertions(+), 41 deletions(-) diff --git a/client/http_client.go b/client/http_client.go index 0c8bb6363..c11f8b3e3 100644 --- a/client/http_client.go +++ b/client/http_client.go @@ -11,8 +11,7 @@ import ( "reflect" "strings" - // cmn "github.com/tendermint/go-common" - rpctypes "github.com/tendermint/go-rpc/types" + types "github.com/tendermint/go-rpc/types" wire "github.com/tendermint/go-wire" ) @@ -28,7 +27,7 @@ func makeHTTPDialer(remoteAddr string) (string, func(string, string) (net.Conn, var protocol, address string if len(parts) != 2 { log.Warn("WARNING (go-rpc): Please use fully formed listening addresses, including the tcp:// or unix:// prefix") - protocol = rpctypes.SocketType(remoteAddr) + protocol = types.SocketType(remoteAddr) address = remoteAddr } else { protocol, address = parts[0], parts[1] @@ -73,7 +72,7 @@ func (c *ClientJSONRPC) Call(method string, params map[string]interface{}, resul func (c *ClientJSONRPC) call(method string, params map[string]interface{}, result interface{}) (interface{}, error) { // Make request and get responseBytes - request := rpctypes.RPCRequest{ + request := types.RPCRequest{ JSONRPC: "2.0", Method: method, Params: params, @@ -144,7 +143,7 @@ func unmarshalResponseBytes(responseBytes []byte, result interface{}) (interface // into the correct type // log.Notice("response", "response", string(responseBytes)) var err error - response := &rpctypes.RPCResponse{} + response := &types.RPCResponse{} err = json.Unmarshal(responseBytes, response) if err != nil { return nil, fmt.Errorf("Error unmarshalling rpc response: %v", err) diff --git a/client/ws_client.go b/client/ws_client.go index e5135d0af..d27e499dd 100644 --- a/client/ws_client.go +++ b/client/ws_client.go @@ -9,7 +9,7 @@ import ( "github.com/gorilla/websocket" cmn "github.com/tendermint/go-common" - rpctypes "github.com/tendermint/go-rpc/types" + types "github.com/tendermint/go-rpc/types" ) const ( @@ -96,7 +96,7 @@ func (wsc *WSClient) receiveEventsRoutine() { wsc.Stop() break } else { - var response rpctypes.RPCResponse + var response types.RPCResponse err := json.Unmarshal(data, &response) if err != nil { log.Info("WSClient failed to parse message", "error", err, "data", string(data)) @@ -118,7 +118,7 @@ func (wsc *WSClient) receiveEventsRoutine() { // subscribe to an event func (wsc *WSClient) Subscribe(eventid string) error { - err := wsc.WriteJSON(rpctypes.RPCRequest{ + err := wsc.WriteJSON(types.RPCRequest{ JSONRPC: "2.0", ID: "", Method: "subscribe", @@ -129,7 +129,7 @@ func (wsc *WSClient) Subscribe(eventid string) error { // unsubscribe from an event func (wsc *WSClient) Unsubscribe(eventid string) error { - err := wsc.WriteJSON(rpctypes.RPCRequest{ + err := wsc.WriteJSON(types.RPCRequest{ JSONRPC: "2.0", ID: "", Method: "unsubscribe", diff --git a/rpc_test.go b/rpc_test.go index 2c45c4c13..c77799199 100644 --- a/rpc_test.go +++ b/rpc_test.go @@ -5,14 +5,14 @@ import ( "testing" "time" - rpcclient "github.com/tendermint/go-rpc/client" - rpcserver "github.com/tendermint/go-rpc/server" - rpctypes "github.com/tendermint/go-rpc/types" + client "github.com/tendermint/go-rpc/client" + server "github.com/tendermint/go-rpc/server" + types "github.com/tendermint/go-rpc/types" wire "github.com/tendermint/go-wire" ) // Client and Server should work over tcp or unix sockets -var ( +const ( tcpAddr = "tcp://0.0.0.0:46657" unixAddr = "unix:///tmp/go-rpc.sock" // NOTE: must remove file for test to run again @@ -32,8 +32,8 @@ var _ = wire.RegisterInterface( ) // Define some routes -var Routes = map[string]*rpcserver.RPCFunc{ - "status": rpcserver.NewRPCFunc(StatusResult, "arg"), +var Routes = map[string]*server.RPCFunc{ + "status": server.NewRPCFunc(StatusResult, "arg"), } // an rpc function @@ -43,23 +43,24 @@ func StatusResult(v string) (Result, error) { // launch unix and tcp servers func init() { + mux := http.NewServeMux() - rpcserver.RegisterRPCFuncs(mux, Routes) - wm := rpcserver.NewWebsocketManager(Routes, nil) + server.RegisterRPCFuncs(mux, Routes) + wm := server.NewWebsocketManager(Routes, nil) mux.HandleFunc(websocketEndpoint, wm.WebsocketHandler) go func() { - _, err := rpcserver.StartHTTPServer(tcpAddr, mux) + _, err := server.StartHTTPServer(tcpAddr, mux) if err != nil { panic(err) } }() mux2 := http.NewServeMux() - rpcserver.RegisterRPCFuncs(mux2, Routes) - wm = rpcserver.NewWebsocketManager(Routes, nil) + server.RegisterRPCFuncs(mux2, Routes) + wm = server.NewWebsocketManager(Routes, nil) mux2.HandleFunc(websocketEndpoint, wm.WebsocketHandler) go func() { - _, err := rpcserver.StartHTTPServer(unixAddr, mux2) + _, err := server.StartHTTPServer(unixAddr, mux2) if err != nil { panic(err) } @@ -70,7 +71,7 @@ func init() { } -func testURI(t *testing.T, cl *rpcclient.ClientURI) { +func testURI(t *testing.T, cl *client.ClientURI) { val := "acbd" params := map[string]interface{}{ "arg": val, @@ -86,7 +87,7 @@ func testURI(t *testing.T, cl *rpcclient.ClientURI) { } } -func testJSONRPC(t *testing.T, cl *rpcclient.ClientJSONRPC) { +func testJSONRPC(t *testing.T, cl *client.ClientJSONRPC) { val := "acbd" params := map[string]interface{}{ "arg": val, @@ -102,12 +103,12 @@ func testJSONRPC(t *testing.T, cl *rpcclient.ClientJSONRPC) { } } -func testWS(t *testing.T, cl *rpcclient.WSClient) { +func testWS(t *testing.T, cl *client.WSClient) { val := "acbd" params := map[string]interface{}{ "arg": val, } - err := cl.WriteJSON(rpctypes.RPCRequest{ + err := cl.WriteJSON(types.RPCRequest{ JSONRPC: "2.0", ID: "", Method: "status", @@ -132,27 +133,27 @@ func testWS(t *testing.T, cl *rpcclient.WSClient) { //------------- func TestURI_TCP(t *testing.T) { - cl := rpcclient.NewClientURI(tcpAddr) + cl := client.NewClientURI(tcpAddr) testURI(t, cl) } func TestURI_UNIX(t *testing.T) { - cl := rpcclient.NewClientURI(unixAddr) + cl := client.NewClientURI(unixAddr) testURI(t, cl) } func TestJSONRPC_TCP(t *testing.T) { - cl := rpcclient.NewClientJSONRPC(tcpAddr) + cl := client.NewClientJSONRPC(tcpAddr) testJSONRPC(t, cl) } func TestJSONRPC_UNIX(t *testing.T) { - cl := rpcclient.NewClientJSONRPC(unixAddr) + cl := client.NewClientJSONRPC(unixAddr) testJSONRPC(t, cl) } func TestWS_TCP(t *testing.T) { - cl := rpcclient.NewWSClient(tcpAddr, websocketEndpoint) + cl := client.NewWSClient(tcpAddr, websocketEndpoint) _, err := cl.Start() if err != nil { t.Fatal(err) @@ -161,7 +162,7 @@ func TestWS_TCP(t *testing.T) { } func TestWS_UNIX(t *testing.T) { - cl := rpcclient.NewWSClient(unixAddr, websocketEndpoint) + cl := client.NewWSClient(unixAddr, websocketEndpoint) _, err := cl.Start() if err != nil { t.Fatal(err) @@ -170,7 +171,7 @@ func TestWS_UNIX(t *testing.T) { } func TestHexStringArg(t *testing.T) { - cl := rpcclient.NewClientURI(tcpAddr) + cl := client.NewClientURI(tcpAddr) // should NOT be handled as hex val := "0xabc" params := map[string]interface{}{ @@ -188,7 +189,7 @@ func TestHexStringArg(t *testing.T) { } func TestQuotedStringArg(t *testing.T) { - cl := rpcclient.NewClientURI(tcpAddr) + cl := client.NewClientURI(tcpAddr) // should NOT be unquoted val := "\"abc\"" params := map[string]interface{}{ diff --git a/server/http_server.go b/server/http_server.go index 26163cf12..24b9f18af 100644 --- a/server/http_server.go +++ b/server/http_server.go @@ -11,9 +11,7 @@ import ( "strings" "time" - . "github.com/tendermint/go-common" - . "github.com/tendermint/go-rpc/types" - //"github.com/tendermint/go-wire" + types "github.com/tendermint/go-rpc/types" ) func StartHTTPServer(listenAddr string, handler http.Handler) (listener net.Listener, err error) { @@ -24,14 +22,14 @@ func StartHTTPServer(listenAddr string, handler http.Handler) (listener net.List log.Warn("WARNING (go-rpc): Please use fully formed listening addresses, including the tcp:// or unix:// prefix") // we used to allow addrs without tcp/unix prefix by checking for a colon // TODO: Deprecate - proto = SocketType(listenAddr) + proto = types.SocketType(listenAddr) addr = listenAddr // return nil, fmt.Errorf("Invalid listener address %s", lisenAddr) } else { proto, addr = parts[0], parts[1] } - log.Notice(Fmt("Starting RPC HTTP server on %s socket %v", proto, addr)) + log.Notice(fmt.Sprintf("Starting RPC HTTP server on %s socket %v", proto, addr)) listener, err = net.Listen(proto, addr) if err != nil { return nil, fmt.Errorf("Failed to listen to %v: %v", listenAddr, err) @@ -47,7 +45,7 @@ func StartHTTPServer(listenAddr string, handler http.Handler) (listener net.List return listener, nil } -func WriteRPCResponseHTTP(w http.ResponseWriter, res RPCResponse) { +func WriteRPCResponseHTTP(w http.ResponseWriter, res types.RPCResponse) { // jsonBytes := wire.JSONBytesPretty(res) jsonBytes, err := json.Marshal(res) if err != nil { @@ -83,13 +81,13 @@ func RecoverAndLogHandler(handler http.Handler) http.Handler { if e := recover(); e != nil { // If RPCResponse - if res, ok := e.(RPCResponse); ok { + if res, ok := e.(types.RPCResponse); ok { WriteRPCResponseHTTP(rww, res) } else { // For the rest, log.Error("Panic in RPC HTTP handler", "error", e, "stack", string(debug.Stack())) rww.WriteHeader(http.StatusInternalServerError) - WriteRPCResponseHTTP(rww, NewRPCResponse("", nil, Fmt("Internal Server Error: %v", e))) + WriteRPCResponseHTTP(rww, types.NewRPCResponse("", nil, fmt.Sprintf("Internal Server Error: %v", e))) } } From c128957723b151972ec65a3d3bf4e168ead09f1b Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Tue, 7 Mar 2017 19:09:58 +0400 Subject: [PATCH 38/73] "must remove file for test to run again" - no way I am doing this by hands, too lazy :) --- rpc_test.go | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/rpc_test.go b/rpc_test.go index c77799199..990122517 100644 --- a/rpc_test.go +++ b/rpc_test.go @@ -2,6 +2,7 @@ package rpc import ( "net/http" + "os/exec" "testing" "time" @@ -13,8 +14,10 @@ import ( // Client and Server should work over tcp or unix sockets const ( - tcpAddr = "tcp://0.0.0.0:46657" - unixAddr = "unix:///tmp/go-rpc.sock" // NOTE: must remove file for test to run again + tcpAddr = "tcp://0.0.0.0:46657" + + unixSocket = "/tmp/go-rpc.sock" + unixAddr = "unix:///tmp/go-rpc.sock" websocketEndpoint = "/websocket/endpoint" ) @@ -43,6 +46,12 @@ func StatusResult(v string) (Result, error) { // launch unix and tcp servers func init() { + cmd := exec.Command("rm", "-f", unixSocket) + err := cmd.Start() + if err != nil { + panic(err) + } + err = cmd.Wait() mux := http.NewServeMux() server.RegisterRPCFuncs(mux, Routes) From 26ccb4c94a0d214f4e1bd635b07c830620f5c6d5 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Tue, 7 Mar 2017 19:16:36 +0400 Subject: [PATCH 39/73] remove private call methods Q: what was the reason to create them? --- client/http_client.go | 8 -------- 1 file changed, 8 deletions(-) diff --git a/client/http_client.go b/client/http_client.go index c11f8b3e3..1585fd3b5 100644 --- a/client/http_client.go +++ b/client/http_client.go @@ -67,10 +67,6 @@ func NewClientJSONRPC(remote string) *ClientJSONRPC { } func (c *ClientJSONRPC) Call(method string, params map[string]interface{}, result interface{}) (interface{}, error) { - return c.call(method, params, result) -} - -func (c *ClientJSONRPC) call(method string, params map[string]interface{}, result interface{}) (interface{}, error) { // Make request and get responseBytes request := types.RPCRequest{ JSONRPC: "2.0", @@ -114,10 +110,6 @@ func NewClientURI(remote string) *ClientURI { } func (c *ClientURI) Call(method string, params map[string]interface{}, result interface{}) (interface{}, error) { - return c.call(method, params, result) -} - -func (c *ClientURI) call(method string, params map[string]interface{}, result interface{}) (interface{}, error) { values, err := argsToURLValues(params) if err != nil { return nil, err From d43e3db9789ba0baac1fbcbd3f9945c32ef4c9e0 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Tue, 7 Mar 2017 19:20:19 +0400 Subject: [PATCH 40/73] fix circleci --- Makefile | 2 +- circle.yml | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index e17fa06ac..759c5cc09 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ PACKAGES=$(shell go list ./...) -all: test +all: get_deps test test: @echo "--> Running go test --race" diff --git a/circle.yml b/circle.yml index 99af678c6..0308a4e79 100644 --- a/circle.yml +++ b/circle.yml @@ -11,12 +11,10 @@ checkout: - rm -rf $REPO - mkdir -p $HOME/.go_workspace/src/github.com/$CIRCLE_PROJECT_USERNAME - mv $HOME/$CIRCLE_PROJECT_REPONAME $REPO - # - git submodule sync - # - git submodule update --init # use submodules dependencies: override: - - "cd $REPO" + - "cd $REPO && make get_deps" test: override: From 22ba8bdef81b246965469374fe0e3521124d0356 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Wed, 8 Mar 2017 10:26:13 +0400 Subject: [PATCH 41/73] fix Call method signature in HTTPClient interface --- client/http_client.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/http_client.go b/client/http_client.go index 1585fd3b5..3eb35aa3c 100644 --- a/client/http_client.go +++ b/client/http_client.go @@ -17,7 +17,7 @@ import ( // HTTPClient is a common interface for ClientJSONRPC and ClientURI. type HTTPClient interface { - Call(method string, params []interface{}, result interface{}) (interface{}, error) + Call(method string, params map[string]interface{}, result interface{}) (interface{}, error) } // TODO: Deprecate support for IP:PORT or /path/to/socket From 51d760f29f4e54283498e2dec433d5a36e0e0814 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Wed, 8 Mar 2017 16:23:38 +0400 Subject: [PATCH 42/73] use local import for testing --- Makefile | 2 +- test/main.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 759c5cc09..a2e3bea7f 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -PACKAGES=$(shell go list ./...) +PACKAGES=$(shell go list ./... | grep -v "test") all: get_deps test diff --git a/test/main.go b/test/main.go index 28de2be88..670e3deae 100644 --- a/test/main.go +++ b/test/main.go @@ -4,8 +4,8 @@ import ( "fmt" "net/http" + rpcserver "../server" cmn "github.com/tendermint/go-common" - rpcserver "github.com/tendermint/go-rpc/server" ) var routes = map[string]*rpcserver.RPCFunc{ From 6d66cc68ed17c50985c4bae24158d9dc7eb4d58e Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Wed, 8 Mar 2017 16:24:04 +0400 Subject: [PATCH 43/73] make sure we are using correct server also remove it afterwards --- test/integration_test.sh | 32 ++++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/test/integration_test.sh b/test/integration_test.sh index 739708068..5c85704be 100755 --- a/test/integration_test.sh +++ b/test/integration_test.sh @@ -9,12 +9,19 @@ DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )" # Change into that dir because we expect that. pushd "$DIR" -go build -o server main.go -./server > /dev/null & +echo "==> Building the server" +go build -o rpcserver main.go + +echo "==> (Re)starting the server" +PID=$(pgrep rpcserver || echo "") +if [[ $PID != "" ]]; then + kill -9 "$PID" +fi +./rpcserver & PID=$! sleep 2 -# simple request +echo "==> simple request" R1=$(curl -s 'http://localhost:8008/hello_world?name="my_world"&num=5') R2=$(curl -s --data @data.json http://localhost:8008) if [[ "$R1" != "$R2" ]]; then @@ -23,10 +30,10 @@ if [[ "$R1" != "$R2" ]]; then echo "R2: $R2" exit 1 else - echo "Success" + echo "OK" fi -# request with 0x-prefixed hex string arg +echo "==> request with 0x-prefixed hex string arg" R1=$(curl -s 'http://localhost:8008/hello_world?name=0x41424344&num=123') R2='{"jsonrpc":"2.0","id":"","result":{"Result":"hi ABCD 123"},"error":""}' if [[ "$R1" != "$R2" ]]; then @@ -35,7 +42,7 @@ if [[ "$R1" != "$R2" ]]; then echo "R2: $R2" exit 1 else - echo "Success" + echo "OK" fi # request with unquoted string arg @@ -47,10 +54,10 @@ if [[ "$R1" != "$R2" ]]; then echo "R2: $R2" exit 1 else - echo "Success" + echo "OK" fi -# request with string type when expecting number arg +echo "==> request with string type when expecting number arg" R1=$(curl -s 'http://localhost:8008/hello_world?name="abcd"&num=0xabcd') R2="{\"jsonrpc\":\"2.0\",\"id\":\"\",\"result\":null,\"error\":\"Error converting http params to args: Got a hex string arg, but expected 'int'\"}" if [[ "$R1" != "$R2" ]]; then @@ -59,8 +66,13 @@ if [[ "$R1" != "$R2" ]]; then echo "R2: $R2" exit 1 else - echo "Success" + echo "OK" fi -kill -9 $PID || exit 0 +echo "==> Stopping the server" +kill -9 $PID + +rm -f rpcserver + popd +exit 0 From 2dc6ab3896e876d7ec2371b3faa6112971e9e849 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Wed, 8 Mar 2017 17:16:01 +0400 Subject: [PATCH 44/73] use golang default if an arg is missing (Refs #7) --- server/handlers.go | 54 +++++++++++++++++++++++----------------- test/integration_test.sh | 19 +++++++++++++- 2 files changed, 49 insertions(+), 24 deletions(-) diff --git a/server/handlers.go b/server/handlers.go index beb664d9f..7085b81e2 100644 --- a/server/handlers.go +++ b/server/handlers.go @@ -141,20 +141,20 @@ func makeJSONRPCHandler(funcMap map[string]*RPCFunc) http.HandlerFunc { // Convert a list of interfaces to properly typed values func jsonParamsToArgs(rpcFunc *RPCFunc, params map[string]interface{}) ([]reflect.Value, error) { - if len(rpcFunc.argNames) != len(params) { - return nil, fmt.Errorf("Expected %v parameters (%v), got %v (%v)", - len(rpcFunc.argNames), rpcFunc.argNames, len(params), params) - } + values := make([]reflect.Value, len(rpcFunc.args)) - values := make([]reflect.Value, len(params)) + // fill each value with default + for i, argType := range rpcFunc.args { + values[i] = reflect.Zero(argType) + } for name, param := range params { i := indexOf(name, rpcFunc.argNames) if -1 == i { return nil, fmt.Errorf("%s is not an argument (args: %v)", name, rpcFunc.argNames) } - ty := rpcFunc.args[i] - v, err := _jsonObjectToArg(ty, param) + argType := rpcFunc.args[i] + v, err := _jsonObjectToArg(argType, param) if err != nil { return nil, err } @@ -176,21 +176,21 @@ func indexOf(value string, values []string) int { // Same as above, but with the first param the websocket connection func jsonParamsToArgsWS(rpcFunc *RPCFunc, params map[string]interface{}, wsCtx types.WSRPCContext) ([]reflect.Value, error) { - if len(rpcFunc.argNames) != len(params) { - return nil, fmt.Errorf("Expected %v parameters (%v), got %v (%v)", - len(rpcFunc.argNames)-1, rpcFunc.argNames[1:], len(params), params) - } - - values := make([]reflect.Value, len(params)+1) + values := make([]reflect.Value, len(rpcFunc.args)) values[0] = reflect.ValueOf(wsCtx) + // fill each value with default + for i, argType := range rpcFunc.args { + values[i+1] = reflect.Zero(argType) + } + for name, param := range params { i := indexOf(name, rpcFunc.argNames) if -1 == i { return nil, fmt.Errorf("%s is not an argument (args: %v)", name, rpcFunc.argNames) } - ty := rpcFunc.args[i+1] - v, err := _jsonObjectToArg(ty, param) + argType := rpcFunc.args[i+1] + v, err := _jsonObjectToArg(argType, param) if err != nil { return nil, err } @@ -245,16 +245,23 @@ func makeHTTPHandler(rpcFunc *RPCFunc) func(http.ResponseWriter, *http.Request) // Covert an http query to a list of properly typed values. // To be properly decoded the arg must be a concrete type from tendermint (if its an interface). func httpParamsToArgs(rpcFunc *RPCFunc, r *http.Request) ([]reflect.Value, error) { - argTypes := rpcFunc.args - argNames := rpcFunc.argNames + values := make([]reflect.Value, len(rpcFunc.args)) - values := make([]reflect.Value, len(argNames)) - for i, name := range argNames { - ty := argTypes[i] + // fill each value with default + for i, argType := range rpcFunc.args { + values[i] = reflect.Zero(argType) + } + + for i, name := range rpcFunc.argNames { + argType := rpcFunc.args[i] arg := GetParam(r, name) - // log.Notice("param to arg", "ty", ty, "name", name, "arg", arg) + // log.Notice("param to arg", "argType", argType, "name", name, "arg", arg) + + if "" == arg { + continue + } - v, err, ok := nonJsonToArg(ty, arg) + v, err, ok := nonJsonToArg(argType, arg) if err != nil { return nil, err } @@ -264,11 +271,12 @@ func httpParamsToArgs(rpcFunc *RPCFunc, r *http.Request) ([]reflect.Value, error } // Pass values to go-wire - values[i], err = _jsonStringToArg(ty, arg) + values[i], err = _jsonStringToArg(argType, arg) if err != nil { return nil, err } } + return values, nil } diff --git a/test/integration_test.sh b/test/integration_test.sh index 5c85704be..ea246c896 100755 --- a/test/integration_test.sh +++ b/test/integration_test.sh @@ -28,6 +28,7 @@ if [[ "$R1" != "$R2" ]]; then echo "responses are not identical:" echo "R1: $R1" echo "R2: $R2" + echo "FAIL" exit 1 else echo "OK" @@ -40,18 +41,33 @@ if [[ "$R1" != "$R2" ]]; then echo "responses are not identical:" echo "R1: $R1" echo "R2: $R2" + echo "FAIL" exit 1 else echo "OK" fi -# request with unquoted string arg +echo "==> request with missing params" +R1=$(curl -s 'http://localhost:8008/hello_world') +R2='{"jsonrpc":"2.0","id":"","result":{"Result":"hi 0"},"error":""}' +if [[ "$R1" != "$R2" ]]; then + echo "responses are not identical:" + echo "R1: $R1" + echo "R2: $R2" + echo "FAIL" + exit 1 +else + echo "OK" +fi + +echo "==> request with unquoted string arg" R1=$(curl -s 'http://localhost:8008/hello_world?name=abcd&num=123') R2="{\"jsonrpc\":\"2.0\",\"id\":\"\",\"result\":null,\"error\":\"Error converting http params to args: invalid character 'a' looking for beginning of value\"}" if [[ "$R1" != "$R2" ]]; then echo "responses are not identical:" echo "R1: $R1" echo "R2: $R2" + echo "FAIL" exit 1 else echo "OK" @@ -64,6 +80,7 @@ if [[ "$R1" != "$R2" ]]; then echo "responses are not identical:" echo "R1: $R1" echo "R2: $R2" + echo "FAIL" exit 1 else echo "OK" From d033cd54b8c48a5621bbe8fa67833612643fb7d4 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Wed, 8 Mar 2017 17:17:00 +0400 Subject: [PATCH 45/73] add editorconfig --- .editorconfig | 15 +++++++++++++++ test/integration_test.sh | 12 ++++++------ 2 files changed, 21 insertions(+), 6 deletions(-) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..d587999e1 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +# top-most EditorConfig file +root = true + +# Unix-style newlines with a newline ending every file +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[Makefile] +indent_style = tab + +[*.sh] +indent_style = tab diff --git a/test/integration_test.sh b/test/integration_test.sh index ea246c896..7c23be7d3 100755 --- a/test/integration_test.sh +++ b/test/integration_test.sh @@ -15,7 +15,7 @@ go build -o rpcserver main.go echo "==> (Re)starting the server" PID=$(pgrep rpcserver || echo "") if [[ $PID != "" ]]; then - kill -9 "$PID" + kill -9 "$PID" fi ./rpcserver & PID=$! @@ -51,13 +51,13 @@ echo "==> request with missing params" R1=$(curl -s 'http://localhost:8008/hello_world') R2='{"jsonrpc":"2.0","id":"","result":{"Result":"hi 0"},"error":""}' if [[ "$R1" != "$R2" ]]; then - echo "responses are not identical:" - echo "R1: $R1" - echo "R2: $R2" + echo "responses are not identical:" + echo "R1: $R1" + echo "R2: $R2" echo "FAIL" - exit 1 + exit 1 else - echo "OK" + echo "OK" fi echo "==> request with unquoted string arg" From 1842e03315833fd61c070ba7ba13fd5bbf51e42b Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Wed, 8 Mar 2017 17:33:46 +0400 Subject: [PATCH 46/73] revert using local import this breaks the client's code (e.g. tendermint) --- test/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/main.go b/test/main.go index 670e3deae..28de2be88 100644 --- a/test/main.go +++ b/test/main.go @@ -4,8 +4,8 @@ import ( "fmt" "net/http" - rpcserver "../server" cmn "github.com/tendermint/go-common" + rpcserver "github.com/tendermint/go-rpc/server" ) var routes = map[string]*rpcserver.RPCFunc{ From fed84f875cfc59a22dfe98e68b93e1fb5151fef9 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Wed, 8 Mar 2017 17:55:08 +0400 Subject: [PATCH 47/73] fix jsonParamsToArgsWS index error Error from tendermint: ``` panic: runtime error: index out of range goroutine 82 [running]: github.com/tendermint/tendermint/vendor/github.com/tendermint/go-rpc/server.jsonParamsToArgsWS(0xc4200960e0, 0xc42024d4a0, 0xc420215380, 0x3, 0x0, 0x0, 0xc420215383, 0x9, 0xc42024d4a0, 0xf1ecc0, ...) /home/vagrant/go/src/github.com/tendermint/tendermint/vendor/github.com/tendermint/go-rpc/server/handlers.go:184 +0x654 github.com/tendermint/tendermint/vendor/github.com/tendermint/go-rpc/server.(*wsConnection).readRoutine(0xc4201fd0e0) /home/vagrant/go/src/github.com/tendermint/tendermint/vendor/github.com/tendermint/go-rpc/server/handlers.go:496 +0x3a9 created by github.com/tendermint/tendermint/vendor/github.com/tendermint/go-rpc/server.(*wsConnection).OnStart /home/vagrant/go/src/github.com/tendermint/tendermint/vendor/github.com/tendermint/go-rpc/server/handlers.go:377 +0x45 ``` --- server/handlers.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/handlers.go b/server/handlers.go index 7085b81e2..2a6102d1e 100644 --- a/server/handlers.go +++ b/server/handlers.go @@ -176,7 +176,7 @@ func indexOf(value string, values []string) int { // Same as above, but with the first param the websocket connection func jsonParamsToArgsWS(rpcFunc *RPCFunc, params map[string]interface{}, wsCtx types.WSRPCContext) ([]reflect.Value, error) { - values := make([]reflect.Value, len(rpcFunc.args)) + values := make([]reflect.Value, len(rpcFunc.args)+1) values[0] = reflect.ValueOf(wsCtx) // fill each value with default @@ -189,7 +189,7 @@ func jsonParamsToArgsWS(rpcFunc *RPCFunc, params map[string]interface{}, wsCtx t if -1 == i { return nil, fmt.Errorf("%s is not an argument (args: %v)", name, rpcFunc.argNames) } - argType := rpcFunc.args[i+1] + argType := rpcFunc.args[i] v, err := _jsonObjectToArg(argType, param) if err != nil { return nil, err From 1ddb60b6e73b2bb5c2d1845eaa763e0a8ad57855 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Thu, 9 Mar 2017 12:23:21 +0400 Subject: [PATCH 48/73] refactor jsonParamsToArgs Suggested in https://github.com/tendermint/go-rpc/pull/9#discussion_r105098390 --- server/handlers.go | 68 ++++++++++++---------------------------------- 1 file changed, 18 insertions(+), 50 deletions(-) diff --git a/server/handlers.go b/server/handlers.go index 2a6102d1e..6e746a26b 100644 --- a/server/handlers.go +++ b/server/handlers.go @@ -143,61 +143,31 @@ func makeJSONRPCHandler(funcMap map[string]*RPCFunc) http.HandlerFunc { func jsonParamsToArgs(rpcFunc *RPCFunc, params map[string]interface{}) ([]reflect.Value, error) { values := make([]reflect.Value, len(rpcFunc.args)) - // fill each value with default - for i, argType := range rpcFunc.args { - values[i] = reflect.Zero(argType) - } - - for name, param := range params { - i := indexOf(name, rpcFunc.argNames) - if -1 == i { - return nil, fmt.Errorf("%s is not an argument (args: %v)", name, rpcFunc.argNames) - } + for i, argName := range rpcFunc.argNames { argType := rpcFunc.args[i] - v, err := _jsonObjectToArg(argType, param) - if err != nil { - return nil, err + + // decode param if provided + if param, ok := params[argName]; ok && "" != param { + v, err := _jsonObjectToArg(argType, param) + if err != nil { + return nil, err + } + values[i] = v + } else { // use default for that type + values[i] = reflect.Zero(argType) } - values[i] = v } return values, nil } -// indexOf returns index of a string in a slice of strings, -1 if not found. -func indexOf(value string, values []string) int { - for i, v := range values { - if v == value { - return i - } - } - return -1 -} - // Same as above, but with the first param the websocket connection func jsonParamsToArgsWS(rpcFunc *RPCFunc, params map[string]interface{}, wsCtx types.WSRPCContext) ([]reflect.Value, error) { - values := make([]reflect.Value, len(rpcFunc.args)+1) - values[0] = reflect.ValueOf(wsCtx) - - // fill each value with default - for i, argType := range rpcFunc.args { - values[i+1] = reflect.Zero(argType) - } - - for name, param := range params { - i := indexOf(name, rpcFunc.argNames) - if -1 == i { - return nil, fmt.Errorf("%s is not an argument (args: %v)", name, rpcFunc.argNames) - } - argType := rpcFunc.args[i] - v, err := _jsonObjectToArg(argType, param) - if err != nil { - return nil, err - } - values[i+1] = v + values, err := jsonParamsToArgs(rpcFunc, params) + if err != nil { + return nil, err } - - return values, nil + return append([]reflect.Value{reflect.ValueOf(wsCtx)}, values...), nil } func _jsonObjectToArg(ty reflect.Type, object interface{}) (reflect.Value, error) { @@ -247,13 +217,11 @@ func makeHTTPHandler(rpcFunc *RPCFunc) func(http.ResponseWriter, *http.Request) func httpParamsToArgs(rpcFunc *RPCFunc, r *http.Request) ([]reflect.Value, error) { values := make([]reflect.Value, len(rpcFunc.args)) - // fill each value with default - for i, argType := range rpcFunc.args { - values[i] = reflect.Zero(argType) - } - for i, name := range rpcFunc.argNames { argType := rpcFunc.args[i] + + values[i] = reflect.Zero(argType) // set default for that type + arg := GetParam(r, name) // log.Notice("param to arg", "argType", argType, "name", name, "arg", arg) From cf11e6ba65423f81b9db3bc2550303d1a325e8ae Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Thu, 9 Mar 2017 12:43:24 +0400 Subject: [PATCH 49/73] add CHANGELOG --- README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/README.md b/README.md index 4da0e5634..149ecdf51 100644 --- a/README.md +++ b/README.md @@ -109,3 +109,18 @@ Each route is available as a GET request, as a JSONRPCv2 POST request, and via J * [Tendermint](https://github.com/tendermint/tendermint/blob/master/rpc/core/routes.go) * [Network Monitor](https://github.com/tendermint/netmon/blob/master/handlers/routes.go) + +## CHANGELOG + +### 0.7.0 + +BREAKING CHANGES: + +- removed `Client` empty interface +- `ClientJSONRPC#Call` `params` argument became a map + +IMPROVEMENTS: + +- added `HTTPClient` interface, which can be used for both `ClientURI` +and `ClientJSONRPC` +- all params are now optional (Golang's default will be used if some param is missing) From 05e1a22d5b6e91c9c00b0824f4f95e0efd0a34da Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Thu, 9 Mar 2017 13:46:48 +0400 Subject: [PATCH 50/73] encode params before sending in JSONRPC --- client/http_client.go | 9 +++++++-- rpc_test.go | 41 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/client/http_client.go b/client/http_client.go index 3eb35aa3c..960869b75 100644 --- a/client/http_client.go +++ b/client/http_client.go @@ -67,11 +67,16 @@ func NewClientJSONRPC(remote string) *ClientJSONRPC { } func (c *ClientJSONRPC) Call(method string, params map[string]interface{}, result interface{}) (interface{}, error) { - // Make request and get responseBytes + // we need this step because we attempt to decode values using `go-wire` + // (handlers.go:176) on the server side + encodedParams := make(map[string]interface{}) + for k, v := range params { + encodedParams[k] = json.RawMessage(wire.JSONBytes(v)) + } request := types.RPCRequest{ JSONRPC: "2.0", Method: method, - Params: params, + Params: encodedParams, ID: "", } requestBytes, err := json.Marshal(request) diff --git a/rpc_test.go b/rpc_test.go index 990122517..a719aee55 100644 --- a/rpc_test.go +++ b/rpc_test.go @@ -1,6 +1,9 @@ package rpc import ( + "bytes" + crand "crypto/rand" + "math/rand" "net/http" "os/exec" "testing" @@ -29,14 +32,20 @@ type ResultStatus struct { Value string } +type ResultBytes struct { + Value []byte +} + var _ = wire.RegisterInterface( struct{ Result }{}, wire.ConcreteType{&ResultStatus{}, 0x1}, + wire.ConcreteType{&ResultBytes{}, 0x2}, ) // Define some routes var Routes = map[string]*server.RPCFunc{ "status": server.NewRPCFunc(StatusResult, "arg"), + "bytes": server.NewRPCFunc(BytesResult, "arg"), } // an rpc function @@ -44,6 +53,10 @@ func StatusResult(v string) (Result, error) { return &ResultStatus{v}, nil } +func BytesResult(v []byte) (Result, error) { + return &ResultBytes{v}, nil +} + // launch unix and tcp servers func init() { cmd := exec.Command("rm", "-f", unixSocket) @@ -214,3 +227,31 @@ func TestQuotedStringArg(t *testing.T) { t.Fatalf("Got: %v .... Expected: %v \n", got, val) } } + +func randBytes(t *testing.T) []byte { + n := rand.Intn(10) + 2 + buf := make([]byte, n) + _, err := crand.Read(buf) + if err != nil { + t.Fatal(err) + } + return bytes.Replace(buf, []byte("="), []byte{100}, -1) +} + +func TestByteSliceViaJSONRPC(t *testing.T) { + cl := client.NewClientJSONRPC(unixAddr) + + val := randBytes(t) + params := map[string]interface{}{ + "arg": val, + } + var result Result + _, err := cl.Call("bytes", params, &result) + if err != nil { + t.Fatal(err) + } + got := result.(*ResultBytes).Value + if bytes.Compare(got, val) != 0 { + t.Fatalf("Got: %v .... Expected: %v \n", got, val) + } +} From 720b74d89edd0b9e7406e7b4557352448b4c0c16 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Thu, 9 Mar 2017 17:44:00 +0400 Subject: [PATCH 51/73] read from ErrorsCh also --- rpc_test.go | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/rpc_test.go b/rpc_test.go index a719aee55..074c212a8 100644 --- a/rpc_test.go +++ b/rpc_test.go @@ -140,16 +140,20 @@ func testWS(t *testing.T, cl *client.WSClient) { t.Fatal(err) } - msg := <-cl.ResultsCh - result := new(Result) - wire.ReadJSONPtr(result, msg, &err) - if err != nil { + select { + case msg := <-cl.ResultsCh: + result := new(Result) + wire.ReadJSONPtr(result, msg, &err) + if err != nil { + t.Fatal(err) + } + got := (*result).(*ResultStatus).Value + if got != val { + t.Fatalf("Got: %v .... Expected: %v \n", got, val) + } + case err := <-cl.ErrorsCh: t.Fatal(err) } - got := (*result).(*ResultStatus).Value - if got != val { - t.Fatalf("Got: %v .... Expected: %v \n", got, val) - } } //------------- From ff90224ba8166ebb2df545af44f3f832ac3452c5 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Thu, 9 Mar 2017 18:30:55 +0400 Subject: [PATCH 52/73] fix "Expected map but got type string" error Error from tendermint: ``` panic: Expected map but got type string [recovered] panic: Expected map but got type string goroutine 82 [running]: testing.tRunner.func1(0xc420464000) /usr/local/go/src/testing/testing.go:622 +0x29d panic(0xa1fda0, 0xc4201eecd0) /usr/local/go/src/runtime/panic.go:489 +0x2cf github.com/tendermint/tendermint/rpc/test.waitForEvent(0xc420464000, 0xc420064000, 0xae6fae, 0x8, 0xae6f01, 0xc2e998, 0xc2e9a0) /home/vagrant/go/src/github.com/tendermint/tendermint/rpc/test/helpers.go:179 +0x53a github.com/tendermint/tendermint/rpc/test.TestWSNewBlock(0xc420464000) /home/vagrant/go/src/github.com/tendermint/tendermint/rpc/test/client_test.go:190 +0x12e testing.tRunner(0xc420464000, 0xc2e9a8) /usr/local/go/src/testing/testing.go:657 +0x96 created by testing.(*T).Run /usr/local/go/src/testing/testing.go:697 +0x2ca ``` --- rpc_test.go | 47 ++++++++++++++++++++++++++++++++++++++++++++-- server/handlers.go | 17 +++++++++++------ 2 files changed, 56 insertions(+), 8 deletions(-) diff --git a/rpc_test.go b/rpc_test.go index 074c212a8..334909f1f 100644 --- a/rpc_test.go +++ b/rpc_test.go @@ -44,8 +44,9 @@ var _ = wire.RegisterInterface( // Define some routes var Routes = map[string]*server.RPCFunc{ - "status": server.NewRPCFunc(StatusResult, "arg"), - "bytes": server.NewRPCFunc(BytesResult, "arg"), + "status": server.NewRPCFunc(StatusResult, "arg"), + "status_ws": server.NewWSRPCFunc(StatusWSResult, "arg"), + "bytes": server.NewRPCFunc(BytesResult, "arg"), } // an rpc function @@ -53,6 +54,10 @@ func StatusResult(v string) (Result, error) { return &ResultStatus{v}, nil } +func StatusWSResult(wsCtx types.WSRPCContext, v string) (Result, error) { + return &ResultStatus{v}, nil +} + func BytesResult(v []byte) (Result, error) { return &ResultBytes{v}, nil } @@ -259,3 +264,41 @@ func TestByteSliceViaJSONRPC(t *testing.T) { t.Fatalf("Got: %v .... Expected: %v \n", got, val) } } + +func TestWSNewWSRPCFunc(t *testing.T) { + cl := client.NewWSClient(unixAddr, websocketEndpoint) + _, err := cl.Start() + if err != nil { + t.Fatal(err) + } + defer cl.Stop() + + val := "acbd" + params := map[string]interface{}{ + "arg": val, + } + err = cl.WriteJSON(types.RPCRequest{ + JSONRPC: "2.0", + ID: "", + Method: "status_ws", + Params: params, + }) + if err != nil { + t.Fatal(err) + } + + select { + case msg := <-cl.ResultsCh: + result := new(Result) + wire.ReadJSONPtr(result, msg, &err) + if err != nil { + t.Fatal(err) + } + got := (*result).(*ResultStatus).Value + if got != val { + t.Fatalf("Got: %v .... Expected: %v \n", got, val) + } + case err := <-cl.ErrorsCh: + t.Fatal(err) + } +} diff --git a/server/handlers.go b/server/handlers.go index 6e746a26b..e590a45fd 100644 --- a/server/handlers.go +++ b/server/handlers.go @@ -123,7 +123,7 @@ func makeJSONRPCHandler(funcMap map[string]*RPCFunc) http.HandlerFunc { WriteRPCResponseHTTP(w, types.NewRPCResponse(request.ID, nil, "RPC method is only for websockets: "+request.Method)) return } - args, err := jsonParamsToArgs(rpcFunc, request.Params) + args, err := jsonParamsToArgs(rpcFunc, request.Params, 0) if err != nil { WriteRPCResponseHTTP(w, types.NewRPCResponse(request.ID, nil, fmt.Sprintf("Error converting json params to arguments: %v", err.Error()))) return @@ -140,11 +140,16 @@ func makeJSONRPCHandler(funcMap map[string]*RPCFunc) http.HandlerFunc { } // Convert a list of interfaces to properly typed values -func jsonParamsToArgs(rpcFunc *RPCFunc, params map[string]interface{}) ([]reflect.Value, error) { - values := make([]reflect.Value, len(rpcFunc.args)) +// +// argsOffset is used in jsonParamsToArgsWS, where len(rpcFunc.args) != len(rpcFunc.argNames). +// Example: +// rpcFunc.args = [rpctypes.WSRPCContext string] +// rpcFunc.argNames = ["arg"] +func jsonParamsToArgs(rpcFunc *RPCFunc, params map[string]interface{}, argsOffset int) ([]reflect.Value, error) { + values := make([]reflect.Value, len(rpcFunc.argNames)) for i, argName := range rpcFunc.argNames { - argType := rpcFunc.args[i] + argType := rpcFunc.args[i+argsOffset] // decode param if provided if param, ok := params[argName]; ok && "" != param { @@ -163,7 +168,7 @@ func jsonParamsToArgs(rpcFunc *RPCFunc, params map[string]interface{}) ([]reflec // Same as above, but with the first param the websocket connection func jsonParamsToArgsWS(rpcFunc *RPCFunc, params map[string]interface{}, wsCtx types.WSRPCContext) ([]reflect.Value, error) { - values, err := jsonParamsToArgs(rpcFunc, params) + values, err := jsonParamsToArgs(rpcFunc, params, 1) if err != nil { return nil, err } @@ -463,7 +468,7 @@ func (wsc *wsConnection) readRoutine() { wsCtx := types.WSRPCContext{Request: request, WSRPCConnection: wsc} args, err = jsonParamsToArgsWS(rpcFunc, request.Params, wsCtx) } else { - args, err = jsonParamsToArgs(rpcFunc, request.Params) + args, err = jsonParamsToArgs(rpcFunc, request.Params, 0) } if err != nil { wsc.WriteRPCResponse(types.NewRPCResponse(request.ID, nil, err.Error())) From db69845ded7b5e61d0ea49f58430b7f27e084ed6 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Thu, 9 Mar 2017 19:00:05 +0400 Subject: [PATCH 53/73] introduce errors pkg --- client/http_client.go | 7 ++++--- client/ws_client.go | 4 ++-- server/handlers.go | 5 +++-- server/http_params.go | 15 ++++++++------- server/http_server.go | 5 +++-- 5 files changed, 20 insertions(+), 16 deletions(-) diff --git a/client/http_client.go b/client/http_client.go index 960869b75..7df09f249 100644 --- a/client/http_client.go +++ b/client/http_client.go @@ -11,6 +11,7 @@ import ( "reflect" "strings" + "github.com/pkg/errors" types "github.com/tendermint/go-rpc/types" wire "github.com/tendermint/go-wire" ) @@ -143,16 +144,16 @@ func unmarshalResponseBytes(responseBytes []byte, result interface{}) (interface response := &types.RPCResponse{} err = json.Unmarshal(responseBytes, response) if err != nil { - return nil, fmt.Errorf("Error unmarshalling rpc response: %v", err) + return nil, errors.Errorf("Error unmarshalling rpc response: %v", err) } errorStr := response.Error if errorStr != "" { - return nil, fmt.Errorf("Response error: %v", errorStr) + return nil, errors.Errorf("Response error: %v", errorStr) } // unmarshal the RawMessage into the result result = wire.ReadJSONPtr(result, *response.Result, &err) if err != nil { - return nil, fmt.Errorf("Error unmarshalling rpc response result: %v", err) + return nil, errors.Errorf("Error unmarshalling rpc response result: %v", err) } return result, nil } diff --git a/client/ws_client.go b/client/ws_client.go index d27e499dd..b56547dd6 100644 --- a/client/ws_client.go +++ b/client/ws_client.go @@ -2,12 +2,12 @@ package rpcclient import ( "encoding/json" - "fmt" "net" "net/http" "time" "github.com/gorilla/websocket" + "github.com/pkg/errors" cmn "github.com/tendermint/go-common" types "github.com/tendermint/go-rpc/types" ) @@ -104,7 +104,7 @@ func (wsc *WSClient) receiveEventsRoutine() { continue } if response.Error != "" { - wsc.ErrorsCh <- fmt.Errorf(response.Error) + wsc.ErrorsCh <- errors.Errorf(response.Error) continue } wsc.ResultsCh <- *response.Result diff --git a/server/handlers.go b/server/handlers.go index e590a45fd..ca42b2e67 100644 --- a/server/handlers.go +++ b/server/handlers.go @@ -13,6 +13,7 @@ import ( "time" "github.com/gorilla/websocket" + "github.com/pkg/errors" cmn "github.com/tendermint/go-common" events "github.com/tendermint/go-events" types "github.com/tendermint/go-rpc/types" @@ -272,7 +273,7 @@ func nonJsonToArg(ty reflect.Type, arg string) (reflect.Value, error, bool) { if isHexString { if !expectingString && !expectingByteSlice { - err := fmt.Errorf("Got a hex string arg, but expected '%s'", + err := errors.Errorf("Got a hex string arg, but expected '%s'", ty.Kind().String()) return reflect.ValueOf(nil), err, false } @@ -567,7 +568,7 @@ func (wm *WebsocketManager) WebsocketHandler(w http.ResponseWriter, r *http.Requ func unreflectResult(returns []reflect.Value) (interface{}, error) { errV := returns[1] if errV.Interface() != nil { - return nil, fmt.Errorf("%v", errV.Interface()) + return nil, errors.Errorf("%v", errV.Interface()) } rv := returns[0] // the result is a registered interface, diff --git a/server/http_params.go b/server/http_params.go index acf5b4c8c..565060678 100644 --- a/server/http_params.go +++ b/server/http_params.go @@ -2,10 +2,11 @@ package rpcserver import ( "encoding/hex" - "fmt" "net/http" "regexp" "strconv" + + "github.com/pkg/errors" ) var ( @@ -39,7 +40,7 @@ func GetParamInt64(r *http.Request, param string) (int64, error) { s := GetParam(r, param) i, err := strconv.ParseInt(s, 10, 64) if err != nil { - return 0, fmt.Errorf(param, err.Error()) + return 0, errors.Errorf(param, err.Error()) } return i, nil } @@ -48,7 +49,7 @@ func GetParamInt32(r *http.Request, param string) (int32, error) { s := GetParam(r, param) i, err := strconv.ParseInt(s, 10, 32) if err != nil { - return 0, fmt.Errorf(param, err.Error()) + return 0, errors.Errorf(param, err.Error()) } return int32(i), nil } @@ -57,7 +58,7 @@ func GetParamUint64(r *http.Request, param string) (uint64, error) { s := GetParam(r, param) i, err := strconv.ParseUint(s, 10, 64) if err != nil { - return 0, fmt.Errorf(param, err.Error()) + return 0, errors.Errorf(param, err.Error()) } return i, nil } @@ -66,7 +67,7 @@ func GetParamUint(r *http.Request, param string) (uint, error) { s := GetParam(r, param) i, err := strconv.ParseUint(s, 10, 64) if err != nil { - return 0, fmt.Errorf(param, err.Error()) + return 0, errors.Errorf(param, err.Error()) } return uint(i), nil } @@ -74,7 +75,7 @@ func GetParamUint(r *http.Request, param string) (uint, error) { func GetParamRegexp(r *http.Request, param string, re *regexp.Regexp) (string, error) { s := GetParam(r, param) if !re.MatchString(s) { - return "", fmt.Errorf(param, "Did not match regular expression %v", re.String()) + return "", errors.Errorf(param, "Did not match regular expression %v", re.String()) } return s, nil } @@ -83,7 +84,7 @@ func GetParamFloat64(r *http.Request, param string) (float64, error) { s := GetParam(r, param) f, err := strconv.ParseFloat(s, 64) if err != nil { - return 0, fmt.Errorf(param, err.Error()) + return 0, errors.Errorf(param, err.Error()) } return f, nil } diff --git a/server/http_server.go b/server/http_server.go index 24b9f18af..5375c574f 100644 --- a/server/http_server.go +++ b/server/http_server.go @@ -11,6 +11,7 @@ import ( "strings" "time" + "github.com/pkg/errors" types "github.com/tendermint/go-rpc/types" ) @@ -24,7 +25,7 @@ func StartHTTPServer(listenAddr string, handler http.Handler) (listener net.List // TODO: Deprecate proto = types.SocketType(listenAddr) addr = listenAddr - // return nil, fmt.Errorf("Invalid listener address %s", lisenAddr) + // return nil, errors.Errorf("Invalid listener address %s", lisenAddr) } else { proto, addr = parts[0], parts[1] } @@ -32,7 +33,7 @@ func StartHTTPServer(listenAddr string, handler http.Handler) (listener net.List log.Notice(fmt.Sprintf("Starting RPC HTTP server on %s socket %v", proto, addr)) listener, err = net.Listen(proto, addr) if err != nil { - return nil, fmt.Errorf("Failed to listen to %v: %v", listenAddr, err) + return nil, errors.Errorf("Failed to listen to %v: %v", listenAddr, err) } go func() { From 715f78e26a2d64ab52569867e0fd8b3bc7a4256f Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Thu, 9 Mar 2017 21:00:25 +0100 Subject: [PATCH 54/73] Properly encode json.RawMessage --- client/http_client.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/client/http_client.go b/client/http_client.go index 7df09f249..5030323b5 100644 --- a/client/http_client.go +++ b/client/http_client.go @@ -72,7 +72,9 @@ func (c *ClientJSONRPC) Call(method string, params map[string]interface{}, resul // (handlers.go:176) on the server side encodedParams := make(map[string]interface{}) for k, v := range params { - encodedParams[k] = json.RawMessage(wire.JSONBytes(v)) + // log.Printf("%s: %v (%s)\n", k, v, string(wire.JSONBytes(v))) + bytes := json.RawMessage(wire.JSONBytes(v)) + encodedParams[k] = &bytes } request := types.RPCRequest{ JSONRPC: "2.0", @@ -84,6 +86,7 @@ func (c *ClientJSONRPC) Call(method string, params map[string]interface{}, resul if err != nil { return nil, err } + // log.Info(string(requestBytes)) requestBuf := bytes.NewBuffer(requestBytes) // log.Info(Fmt("RPC request to %v (%v): %v", c.remote, method, string(requestBytes))) httpResponse, err := c.client.Post(c.address, "text/json", requestBuf) From e6c083f589d93d5cada7c5e9fff1c3c05873a167 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Wed, 8 Mar 2017 01:22:35 +0400 Subject: [PATCH 55/73] rename ClientURI -> URIClient, ClientJSONRPC -> JSONRPCClient (Refs #4) --- README.md | 1 + client/http_client.go | 18 +++++++++--------- rpc_test.go | 18 +++++++++--------- 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 149ecdf51..06a679032 100644 --- a/README.md +++ b/README.md @@ -118,6 +118,7 @@ BREAKING CHANGES: - removed `Client` empty interface - `ClientJSONRPC#Call` `params` argument became a map +- rename `ClientURI` -> `URIClient`, `ClientJSONRPC` -> `JSONRPCClient` IMPROVEMENTS: diff --git a/client/http_client.go b/client/http_client.go index 5030323b5..96bae9d9b 100644 --- a/client/http_client.go +++ b/client/http_client.go @@ -16,7 +16,7 @@ import ( wire "github.com/tendermint/go-wire" ) -// HTTPClient is a common interface for ClientJSONRPC and ClientURI. +// HTTPClient is a common interface for JSONRPCClient and URIClient. type HTTPClient interface { Call(method string, params map[string]interface{}, result interface{}) (interface{}, error) } @@ -54,20 +54,20 @@ func makeHTTPClient(remoteAddr string) (string, *http.Client) { //------------------------------------------------------------------------------------ // JSON rpc takes params as a slice -type ClientJSONRPC struct { +type JSONRPCClient struct { address string client *http.Client } -func NewClientJSONRPC(remote string) *ClientJSONRPC { +func NewJSONRPCClient(remote string) *JSONRPCClient { address, client := makeHTTPClient(remote) - return &ClientJSONRPC{ + return &JSONRPCClient{ address: address, client: client, } } -func (c *ClientJSONRPC) Call(method string, params map[string]interface{}, result interface{}) (interface{}, error) { +func (c *JSONRPCClient) Call(method string, params map[string]interface{}, result interface{}) (interface{}, error) { // we need this step because we attempt to decode values using `go-wire` // (handlers.go:176) on the server side encodedParams := make(map[string]interface{}) @@ -105,20 +105,20 @@ func (c *ClientJSONRPC) Call(method string, params map[string]interface{}, resul //------------------------------------------------------------- // URI takes params as a map -type ClientURI struct { +type URIClient struct { address string client *http.Client } -func NewClientURI(remote string) *ClientURI { +func NewURIClient(remote string) *URIClient { address, client := makeHTTPClient(remote) - return &ClientURI{ + return &URIClient{ address: address, client: client, } } -func (c *ClientURI) Call(method string, params map[string]interface{}, result interface{}) (interface{}, error) { +func (c *URIClient) Call(method string, params map[string]interface{}, result interface{}) (interface{}, error) { values, err := argsToURLValues(params) if err != nil { return nil, err diff --git a/rpc_test.go b/rpc_test.go index 334909f1f..b1ae2566d 100644 --- a/rpc_test.go +++ b/rpc_test.go @@ -98,7 +98,7 @@ func init() { } -func testURI(t *testing.T, cl *client.ClientURI) { +func testURI(t *testing.T, cl *client.URIClient) { val := "acbd" params := map[string]interface{}{ "arg": val, @@ -114,7 +114,7 @@ func testURI(t *testing.T, cl *client.ClientURI) { } } -func testJSONRPC(t *testing.T, cl *client.ClientJSONRPC) { +func testJSONRPC(t *testing.T, cl *client.JSONRPCClient) { val := "acbd" params := map[string]interface{}{ "arg": val, @@ -164,22 +164,22 @@ func testWS(t *testing.T, cl *client.WSClient) { //------------- func TestURI_TCP(t *testing.T) { - cl := client.NewClientURI(tcpAddr) + cl := client.NewURIClient(tcpAddr) testURI(t, cl) } func TestURI_UNIX(t *testing.T) { - cl := client.NewClientURI(unixAddr) + cl := client.NewURIClient(unixAddr) testURI(t, cl) } func TestJSONRPC_TCP(t *testing.T) { - cl := client.NewClientJSONRPC(tcpAddr) + cl := client.NewJSONRPCClient(tcpAddr) testJSONRPC(t, cl) } func TestJSONRPC_UNIX(t *testing.T) { - cl := client.NewClientJSONRPC(unixAddr) + cl := client.NewJSONRPCClient(unixAddr) testJSONRPC(t, cl) } @@ -202,7 +202,7 @@ func TestWS_UNIX(t *testing.T) { } func TestHexStringArg(t *testing.T) { - cl := client.NewClientURI(tcpAddr) + cl := client.NewURIClient(tcpAddr) // should NOT be handled as hex val := "0xabc" params := map[string]interface{}{ @@ -220,7 +220,7 @@ func TestHexStringArg(t *testing.T) { } func TestQuotedStringArg(t *testing.T) { - cl := client.NewClientURI(tcpAddr) + cl := client.NewURIClient(tcpAddr) // should NOT be unquoted val := "\"abc\"" params := map[string]interface{}{ @@ -248,7 +248,7 @@ func randBytes(t *testing.T) []byte { } func TestByteSliceViaJSONRPC(t *testing.T) { - cl := client.NewClientJSONRPC(unixAddr) + cl := client.NewJSONRPCClient(unixAddr) val := randBytes(t) params := map[string]interface{}{ From d66ebbd90407d22b52a74711d21073409a91c571 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Fri, 10 Mar 2017 12:03:16 +0400 Subject: [PATCH 56/73] use testify package --- Makefile | 3 ++ rpc_test.go | 82 ++++++++++++++--------------------------------------- 2 files changed, 25 insertions(+), 60 deletions(-) diff --git a/Makefile b/Makefile index a2e3bea7f..0937558a8 100644 --- a/Makefile +++ b/Makefile @@ -11,5 +11,8 @@ test: get_deps: @echo "--> Running go get" @go get -v -d $(PACKAGES) + @go list -f '{{join .TestImports "\n"}}' ./... | \ + grep -v /vendor/ | sort | uniq | \ + xargs go get -v -d .PHONY: all test get_deps diff --git a/rpc_test.go b/rpc_test.go index b1ae2566d..b52794ada 100644 --- a/rpc_test.go +++ b/rpc_test.go @@ -9,6 +9,8 @@ import ( "testing" "time" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" client "github.com/tendermint/go-rpc/client" server "github.com/tendermint/go-rpc/server" types "github.com/tendermint/go-rpc/types" @@ -105,13 +107,9 @@ func testURI(t *testing.T, cl *client.URIClient) { } var result Result _, err := cl.Call("status", params, &result) - if err != nil { - t.Fatal(err) - } + require.Nil(t, err) got := result.(*ResultStatus).Value - if got != val { - t.Fatalf("Got: %v .... Expected: %v \n", got, val) - } + assert.Equal(t, got, val) } func testJSONRPC(t *testing.T, cl *client.JSONRPCClient) { @@ -121,13 +119,9 @@ func testJSONRPC(t *testing.T, cl *client.JSONRPCClient) { } var result Result _, err := cl.Call("status", params, &result) - if err != nil { - t.Fatal(err) - } + require.Nil(t, err) got := result.(*ResultStatus).Value - if got != val { - t.Fatalf("Got: %v .... Expected: %v \n", got, val) - } + assert.Equal(t, got, val) } func testWS(t *testing.T, cl *client.WSClient) { @@ -141,21 +135,15 @@ func testWS(t *testing.T, cl *client.WSClient) { Method: "status", Params: params, }) - if err != nil { - t.Fatal(err) - } + require.Nil(t, err) select { case msg := <-cl.ResultsCh: result := new(Result) wire.ReadJSONPtr(result, msg, &err) - if err != nil { - t.Fatal(err) - } + require.Nil(t, err) got := (*result).(*ResultStatus).Value - if got != val { - t.Fatalf("Got: %v .... Expected: %v \n", got, val) - } + assert.Equal(t, got, val) case err := <-cl.ErrorsCh: t.Fatal(err) } @@ -186,18 +174,14 @@ func TestJSONRPC_UNIX(t *testing.T) { func TestWS_TCP(t *testing.T) { cl := client.NewWSClient(tcpAddr, websocketEndpoint) _, err := cl.Start() - if err != nil { - t.Fatal(err) - } + require.Nil(t, err) testWS(t, cl) } func TestWS_UNIX(t *testing.T) { cl := client.NewWSClient(unixAddr, websocketEndpoint) _, err := cl.Start() - if err != nil { - t.Fatal(err) - } + require.Nil(t, err) testWS(t, cl) } @@ -210,13 +194,9 @@ func TestHexStringArg(t *testing.T) { } var result Result _, err := cl.Call("status", params, &result) - if err != nil { - t.Fatal(err) - } + require.Nil(t, err) got := result.(*ResultStatus).Value - if got != val { - t.Fatalf("Got: %v .... Expected: %v \n", got, val) - } + assert.Equal(t, got, val) } func TestQuotedStringArg(t *testing.T) { @@ -228,22 +208,16 @@ func TestQuotedStringArg(t *testing.T) { } var result Result _, err := cl.Call("status", params, &result) - if err != nil { - t.Fatal(err) - } + require.Nil(t, err) got := result.(*ResultStatus).Value - if got != val { - t.Fatalf("Got: %v .... Expected: %v \n", got, val) - } + assert.Equal(t, got, val) } func randBytes(t *testing.T) []byte { n := rand.Intn(10) + 2 buf := make([]byte, n) _, err := crand.Read(buf) - if err != nil { - t.Fatal(err) - } + require.Nil(t, err) return bytes.Replace(buf, []byte("="), []byte{100}, -1) } @@ -256,21 +230,15 @@ func TestByteSliceViaJSONRPC(t *testing.T) { } var result Result _, err := cl.Call("bytes", params, &result) - if err != nil { - t.Fatal(err) - } + require.Nil(t, err) got := result.(*ResultBytes).Value - if bytes.Compare(got, val) != 0 { - t.Fatalf("Got: %v .... Expected: %v \n", got, val) - } + assert.Equal(t, got, val) } func TestWSNewWSRPCFunc(t *testing.T) { cl := client.NewWSClient(unixAddr, websocketEndpoint) _, err := cl.Start() - if err != nil { - t.Fatal(err) - } + require.Nil(t, err) defer cl.Stop() val := "acbd" @@ -283,21 +251,15 @@ func TestWSNewWSRPCFunc(t *testing.T) { Method: "status_ws", Params: params, }) - if err != nil { - t.Fatal(err) - } + require.Nil(t, err) select { case msg := <-cl.ResultsCh: result := new(Result) wire.ReadJSONPtr(result, msg, &err) - if err != nil { - t.Fatal(err) - } + require.Nil(t, err) got := (*result).(*ResultStatus).Value - if got != val { - t.Fatalf("Got: %v .... Expected: %v \n", got, val) - } + assert.Equal(t, got, val) case err := <-cl.ErrorsCh: t.Fatal(err) } From 0874c72819f0b78ded043e05a4cdce3b973230e0 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Fri, 10 Mar 2017 12:52:40 +0400 Subject: [PATCH 57/73] refactor tests --- rpc_test.go | 83 +++++++++++++++++------------------------------------ 1 file changed, 27 insertions(+), 56 deletions(-) diff --git a/rpc_test.go b/rpc_test.go index b52794ada..acbf440d0 100644 --- a/rpc_test.go +++ b/rpc_test.go @@ -3,6 +3,7 @@ package rpc import ( "bytes" crand "crypto/rand" + "fmt" "math/rand" "net/http" "os/exec" @@ -100,31 +101,25 @@ func init() { } -func testURI(t *testing.T, cl *client.URIClient) { - val := "acbd" +func status(cl client.HTTPClient, val string) (string, error) { params := map[string]interface{}{ "arg": val, } var result Result - _, err := cl.Call("status", params, &result) - require.Nil(t, err) - got := result.(*ResultStatus).Value - assert.Equal(t, got, val) + if _, err := cl.Call("status", params, &result); err != nil { + return "", err + } + return result.(*ResultStatus).Value, nil } -func testJSONRPC(t *testing.T, cl *client.JSONRPCClient) { +func testWithHTTPClient(t *testing.T, cl client.HTTPClient) { val := "acbd" - params := map[string]interface{}{ - "arg": val, - } - var result Result - _, err := cl.Call("status", params, &result) + got, err := status(cl, val) require.Nil(t, err) - got := result.(*ResultStatus).Value assert.Equal(t, got, val) } -func testWS(t *testing.T, cl *client.WSClient) { +func testWithWSClient(t *testing.T, cl *client.WSClient) { val := "acbd" params := map[string]interface{}{ "arg": val, @@ -151,51 +146,32 @@ func testWS(t *testing.T, cl *client.WSClient) { //------------- -func TestURI_TCP(t *testing.T) { - cl := client.NewURIClient(tcpAddr) - testURI(t, cl) -} - -func TestURI_UNIX(t *testing.T) { - cl := client.NewURIClient(unixAddr) - testURI(t, cl) -} - -func TestJSONRPC_TCP(t *testing.T) { - cl := client.NewJSONRPCClient(tcpAddr) - testJSONRPC(t, cl) -} +func TestServersAndClientsBasic(t *testing.T) { + serverAddrs := [...]string{tcpAddr, unixAddr} + for _, addr := range serverAddrs { + cl1 := client.NewURIClient(addr) + fmt.Printf("=== testing server on %s using %v client", addr, cl1) + testWithHTTPClient(t, cl1) -func TestJSONRPC_UNIX(t *testing.T) { - cl := client.NewJSONRPCClient(unixAddr) - testJSONRPC(t, cl) -} + cl2 := client.NewJSONRPCClient(tcpAddr) + fmt.Printf("=== testing server on %s using %v client", addr, cl2) + testWithHTTPClient(t, cl2) -func TestWS_TCP(t *testing.T) { - cl := client.NewWSClient(tcpAddr, websocketEndpoint) - _, err := cl.Start() - require.Nil(t, err) - testWS(t, cl) -} - -func TestWS_UNIX(t *testing.T) { - cl := client.NewWSClient(unixAddr, websocketEndpoint) - _, err := cl.Start() - require.Nil(t, err) - testWS(t, cl) + cl3 := client.NewWSClient(tcpAddr, websocketEndpoint) + _, err := cl3.Start() + require.Nil(t, err) + fmt.Printf("=== testing server on %s using %v client", addr, cl3) + testWithWSClient(t, cl3) + cl3.Stop() + } } func TestHexStringArg(t *testing.T) { cl := client.NewURIClient(tcpAddr) // should NOT be handled as hex val := "0xabc" - params := map[string]interface{}{ - "arg": val, - } - var result Result - _, err := cl.Call("status", params, &result) + got, err := status(cl, val) require.Nil(t, err) - got := result.(*ResultStatus).Value assert.Equal(t, got, val) } @@ -203,13 +179,8 @@ func TestQuotedStringArg(t *testing.T) { cl := client.NewURIClient(tcpAddr) // should NOT be unquoted val := "\"abc\"" - params := map[string]interface{}{ - "arg": val, - } - var result Result - _, err := cl.Call("status", params, &result) + got, err := status(cl, val) require.Nil(t, err) - got := result.(*ResultStatus).Value assert.Equal(t, got, val) } From c88257b0384f6ceeb1219c527cb0ce06402acb1d Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Fri, 10 Mar 2017 12:57:14 +0400 Subject: [PATCH 58/73] rename rpc function status to echo echo means we're returning the input, which is exactly what this function does. --- rpc_test.go | 51 +++++++++++++++++++++++++-------------------------- 1 file changed, 25 insertions(+), 26 deletions(-) diff --git a/rpc_test.go b/rpc_test.go index acbf440d0..7b043953c 100644 --- a/rpc_test.go +++ b/rpc_test.go @@ -31,38 +31,37 @@ const ( // Define a type for results and register concrete versions type Result interface{} -type ResultStatus struct { +type ResultEcho struct { Value string } -type ResultBytes struct { +type ResultEchoBytes struct { Value []byte } var _ = wire.RegisterInterface( struct{ Result }{}, - wire.ConcreteType{&ResultStatus{}, 0x1}, - wire.ConcreteType{&ResultBytes{}, 0x2}, + wire.ConcreteType{&ResultEcho{}, 0x1}, + wire.ConcreteType{&ResultEchoBytes{}, 0x2}, ) // Define some routes var Routes = map[string]*server.RPCFunc{ - "status": server.NewRPCFunc(StatusResult, "arg"), - "status_ws": server.NewWSRPCFunc(StatusWSResult, "arg"), - "bytes": server.NewRPCFunc(BytesResult, "arg"), + "echo": server.NewRPCFunc(EchoResult, "arg"), + "echo_ws": server.NewWSRPCFunc(EchoWSResult, "arg"), + "echo_bytes": server.NewRPCFunc(EchoBytesResult, "arg"), } -// an rpc function -func StatusResult(v string) (Result, error) { - return &ResultStatus{v}, nil +func EchoResult(v string) (Result, error) { + return &ResultEcho{v}, nil } -func StatusWSResult(wsCtx types.WSRPCContext, v string) (Result, error) { - return &ResultStatus{v}, nil +func EchoWSResult(wsCtx types.WSRPCContext, v string) (Result, error) { + return &ResultEcho{v}, nil } -func BytesResult(v []byte) (Result, error) { - return &ResultBytes{v}, nil +func EchoBytesResult(v []byte) (Result, error) { + return &ResultEchoBytes{v}, nil } // launch unix and tcp servers @@ -101,20 +100,20 @@ func init() { } -func status(cl client.HTTPClient, val string) (string, error) { +func echo(cl client.HTTPClient, val string) (string, error) { params := map[string]interface{}{ "arg": val, } var result Result - if _, err := cl.Call("status", params, &result); err != nil { + if _, err := cl.Call("echo", params, &result); err != nil { return "", err } - return result.(*ResultStatus).Value, nil + return result.(*ResultEcho).Value, nil } func testWithHTTPClient(t *testing.T, cl client.HTTPClient) { val := "acbd" - got, err := status(cl, val) + got, err := echo(cl, val) require.Nil(t, err) assert.Equal(t, got, val) } @@ -127,7 +126,7 @@ func testWithWSClient(t *testing.T, cl *client.WSClient) { err := cl.WriteJSON(types.RPCRequest{ JSONRPC: "2.0", ID: "", - Method: "status", + Method: "echo", Params: params, }) require.Nil(t, err) @@ -137,7 +136,7 @@ func testWithWSClient(t *testing.T, cl *client.WSClient) { result := new(Result) wire.ReadJSONPtr(result, msg, &err) require.Nil(t, err) - got := (*result).(*ResultStatus).Value + got := (*result).(*ResultEcho).Value assert.Equal(t, got, val) case err := <-cl.ErrorsCh: t.Fatal(err) @@ -170,7 +169,7 @@ func TestHexStringArg(t *testing.T) { cl := client.NewURIClient(tcpAddr) // should NOT be handled as hex val := "0xabc" - got, err := status(cl, val) + got, err := echo(cl, val) require.Nil(t, err) assert.Equal(t, got, val) } @@ -179,7 +178,7 @@ func TestQuotedStringArg(t *testing.T) { cl := client.NewURIClient(tcpAddr) // should NOT be unquoted val := "\"abc\"" - got, err := status(cl, val) + got, err := echo(cl, val) require.Nil(t, err) assert.Equal(t, got, val) } @@ -200,9 +199,9 @@ func TestByteSliceViaJSONRPC(t *testing.T) { "arg": val, } var result Result - _, err := cl.Call("bytes", params, &result) + _, err := cl.Call("echo_bytes", params, &result) require.Nil(t, err) - got := result.(*ResultBytes).Value + got := result.(*ResultEchoBytes).Value assert.Equal(t, got, val) } @@ -219,7 +218,7 @@ func TestWSNewWSRPCFunc(t *testing.T) { err = cl.WriteJSON(types.RPCRequest{ JSONRPC: "2.0", ID: "", - Method: "status_ws", + Method: "echo_ws", Params: params, }) require.Nil(t, err) @@ -229,7 +228,7 @@ func TestWSNewWSRPCFunc(t *testing.T) { result := new(Result) wire.ReadJSONPtr(result, msg, &err) require.Nil(t, err) - got := (*result).(*ResultStatus).Value + got := (*result).(*ResultEcho).Value assert.Equal(t, got, val) case err := <-cl.ErrorsCh: t.Fatal(err) From 3233c9c003b8ed81ce2e9d18d39fdb5164d2d62a Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Fri, 10 Mar 2017 14:56:04 +0400 Subject: [PATCH 59/73] WSClient failed to "echo_bytes" Error: ``` Expected nil, but got: encoding/hex: invalid byte: U+0078 'x' ``` --- rpc_test.go | 112 ++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 77 insertions(+), 35 deletions(-) diff --git a/rpc_test.go b/rpc_test.go index 7b043953c..41952fca4 100644 --- a/rpc_test.go +++ b/rpc_test.go @@ -97,10 +97,9 @@ func init() { // wait for servers to start time.Sleep(time.Second * 2) - } -func echo(cl client.HTTPClient, val string) (string, error) { +func echoViaHTTP(cl client.HTTPClient, val string) (string, error) { params := map[string]interface{}{ "arg": val, } @@ -111,15 +110,30 @@ func echo(cl client.HTTPClient, val string) (string, error) { return result.(*ResultEcho).Value, nil } +func echoBytesViaHTTP(cl client.HTTPClient, bytes []byte) ([]byte, error) { + params := map[string]interface{}{ + "arg": bytes, + } + var result Result + if _, err := cl.Call("echo_bytes", params, &result); err != nil { + return []byte{}, err + } + return result.(*ResultEchoBytes).Value, nil +} + func testWithHTTPClient(t *testing.T, cl client.HTTPClient) { val := "acbd" - got, err := echo(cl, val) + got, err := echoViaHTTP(cl, val) require.Nil(t, err) assert.Equal(t, got, val) + + val2 := randBytes(t) + got2, err := echoBytesViaHTTP(cl, val2) + require.Nil(t, err) + assert.Equal(t, got2, val2) } -func testWithWSClient(t *testing.T, cl *client.WSClient) { - val := "acbd" +func echoViaWS(cl *client.WSClient, val string) (string, error) { params := map[string]interface{}{ "arg": val, } @@ -129,20 +143,62 @@ func testWithWSClient(t *testing.T, cl *client.WSClient) { Method: "echo", Params: params, }) - require.Nil(t, err) + if err != nil { + return "", err + } select { case msg := <-cl.ResultsCh: result := new(Result) wire.ReadJSONPtr(result, msg, &err) - require.Nil(t, err) - got := (*result).(*ResultEcho).Value - assert.Equal(t, got, val) + if err != nil { + return "", nil + } + return (*result).(*ResultEcho).Value, nil case err := <-cl.ErrorsCh: - t.Fatal(err) + return "", err } } +func echoBytesViaWS(cl *client.WSClient, bytes []byte) ([]byte, error) { + params := map[string]interface{}{ + "arg": bytes, + } + err := cl.WriteJSON(types.RPCRequest{ + JSONRPC: "2.0", + ID: "", + Method: "echo_bytes", + Params: params, + }) + if err != nil { + return []byte{}, err + } + + select { + case msg := <-cl.ResultsCh: + result := new(Result) + wire.ReadJSONPtr(result, msg, &err) + if err != nil { + return []byte{}, nil + } + return (*result).(*ResultEchoBytes).Value, nil + case err := <-cl.ErrorsCh: + return []byte{}, err + } +} + +func testWithWSClient(t *testing.T, cl *client.WSClient) { + val := "acbd" + got, err := echoViaWS(cl, val) + require.Nil(t, err) + assert.Equal(t, got, val) + + val2 := randBytes(t) + got2, err := echoBytesViaWS(cl, val2) + require.Nil(t, err) + assert.Equal(t, got2, val2) +} + //------------- func TestServersAndClientsBasic(t *testing.T) { @@ -169,7 +225,7 @@ func TestHexStringArg(t *testing.T) { cl := client.NewURIClient(tcpAddr) // should NOT be handled as hex val := "0xabc" - got, err := echo(cl, val) + got, err := echoViaHTTP(cl, val) require.Nil(t, err) assert.Equal(t, got, val) } @@ -178,35 +234,13 @@ func TestQuotedStringArg(t *testing.T) { cl := client.NewURIClient(tcpAddr) // should NOT be unquoted val := "\"abc\"" - got, err := echo(cl, val) + got, err := echoViaHTTP(cl, val) require.Nil(t, err) assert.Equal(t, got, val) } -func randBytes(t *testing.T) []byte { - n := rand.Intn(10) + 2 - buf := make([]byte, n) - _, err := crand.Read(buf) - require.Nil(t, err) - return bytes.Replace(buf, []byte("="), []byte{100}, -1) -} - -func TestByteSliceViaJSONRPC(t *testing.T) { - cl := client.NewJSONRPCClient(unixAddr) - - val := randBytes(t) - params := map[string]interface{}{ - "arg": val, - } - var result Result - _, err := cl.Call("echo_bytes", params, &result) - require.Nil(t, err) - got := result.(*ResultEchoBytes).Value - assert.Equal(t, got, val) -} - func TestWSNewWSRPCFunc(t *testing.T) { - cl := client.NewWSClient(unixAddr, websocketEndpoint) + cl := client.NewWSClient(tcpAddr, websocketEndpoint) _, err := cl.Start() require.Nil(t, err) defer cl.Stop() @@ -234,3 +268,11 @@ func TestWSNewWSRPCFunc(t *testing.T) { t.Fatal(err) } } + +func randBytes(t *testing.T) []byte { + n := rand.Intn(10) + 2 + buf := make([]byte, n) + _, err := crand.Read(buf) + require.Nil(t, err) + return bytes.Replace(buf, []byte("="), []byte{100}, -1) +} From 5d19a008ce4ea633b2002404df1e6b6be298f886 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Fri, 10 Mar 2017 15:23:43 +0400 Subject: [PATCH 60/73] add Call method to WSClient, which does proper encoding of params --- README.md | 1 + client/http_client.go | 1 - client/ws_client.go | 26 ++++++++++++++++++++++++-- rpc_test.go | 14 ++------------ 4 files changed, 27 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 06a679032..5928e6fe0 100644 --- a/README.md +++ b/README.md @@ -125,3 +125,4 @@ IMPROVEMENTS: - added `HTTPClient` interface, which can be used for both `ClientURI` and `ClientJSONRPC` - all params are now optional (Golang's default will be used if some param is missing) +- added `Call` method to `WSClient` (see method's doc for details) diff --git a/client/http_client.go b/client/http_client.go index 96bae9d9b..f4a2a6d7e 100644 --- a/client/http_client.go +++ b/client/http_client.go @@ -72,7 +72,6 @@ func (c *JSONRPCClient) Call(method string, params map[string]interface{}, resul // (handlers.go:176) on the server side encodedParams := make(map[string]interface{}) for k, v := range params { - // log.Printf("%s: %v (%s)\n", k, v, string(wire.JSONBytes(v))) bytes := json.RawMessage(wire.JSONBytes(v)) encodedParams[k] = &bytes } diff --git a/client/ws_client.go b/client/ws_client.go index b56547dd6..ecf641221 100644 --- a/client/ws_client.go +++ b/client/ws_client.go @@ -10,6 +10,7 @@ import ( "github.com/pkg/errors" cmn "github.com/tendermint/go-common" types "github.com/tendermint/go-rpc/types" + wire "github.com/tendermint/go-wire" ) const ( @@ -116,7 +117,8 @@ func (wsc *WSClient) receiveEventsRoutine() { close(wsc.ErrorsCh) } -// subscribe to an event +// Subscribe to an event. Note the server must have a "subscribe" route +// defined. func (wsc *WSClient) Subscribe(eventid string) error { err := wsc.WriteJSON(types.RPCRequest{ JSONRPC: "2.0", @@ -127,7 +129,8 @@ func (wsc *WSClient) Subscribe(eventid string) error { return err } -// unsubscribe from an event +// Unsubscribe from an event. Note the server must have a "unsubscribe" route +// defined. func (wsc *WSClient) Unsubscribe(eventid string) error { err := wsc.WriteJSON(types.RPCRequest{ JSONRPC: "2.0", @@ -137,3 +140,22 @@ func (wsc *WSClient) Unsubscribe(eventid string) error { }) return err } + +// Call asynchronously calls a given method by sending an RPCRequest to the +// server. Results will be available on ResultsCh, errors, if any, on ErrorsCh. +func (wsc *WSClient) Call(method string, params map[string]interface{}) error { + // we need this step because we attempt to decode values using `go-wire` + // (handlers.go:470) on the server side + encodedParams := make(map[string]interface{}) + for k, v := range params { + bytes := json.RawMessage(wire.JSONBytes(v)) + encodedParams[k] = &bytes + } + err := wsc.WriteJSON(types.RPCRequest{ + JSONRPC: "2.0", + Method: method, + Params: encodedParams, + ID: "", + }) + return err +} diff --git a/rpc_test.go b/rpc_test.go index 41952fca4..8a05d7295 100644 --- a/rpc_test.go +++ b/rpc_test.go @@ -137,12 +137,7 @@ func echoViaWS(cl *client.WSClient, val string) (string, error) { params := map[string]interface{}{ "arg": val, } - err := cl.WriteJSON(types.RPCRequest{ - JSONRPC: "2.0", - ID: "", - Method: "echo", - Params: params, - }) + err := cl.Call("echo", params) if err != nil { return "", err } @@ -164,12 +159,7 @@ func echoBytesViaWS(cl *client.WSClient, bytes []byte) ([]byte, error) { params := map[string]interface{}{ "arg": bytes, } - err := cl.WriteJSON(types.RPCRequest{ - JSONRPC: "2.0", - ID: "", - Method: "echo_bytes", - Params: params, - }) + err := cl.Call("echo_bytes", params) if err != nil { return []byte{}, err } From b54b9b4ecc2c444f7b11077f24e5abafdcc98676 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Mon, 13 Mar 2017 14:25:57 +0400 Subject: [PATCH 61/73] update url to network monitor [ci skip] [circleci skip] --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 06a679032..cb869d517 100644 --- a/README.md +++ b/README.md @@ -108,7 +108,7 @@ Each route is available as a GET request, as a JSONRPCv2 POST request, and via J # Examples * [Tendermint](https://github.com/tendermint/tendermint/blob/master/rpc/core/routes.go) -* [Network Monitor](https://github.com/tendermint/netmon/blob/master/handlers/routes.go) +* [tm-monitor](https://github.com/tendermint/tools/blob/master/tm-monitor/rpc.go) ## CHANGELOG From afc39febed0c51e01ba2474980533a5c5a899258 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Tue, 21 Mar 2017 20:47:12 +0400 Subject: [PATCH 62/73] close ws connection on Stop --- client/ws_client.go | 2 ++ server/handlers.go | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/client/ws_client.go b/client/ws_client.go index b56547dd6..9ed2be8c5 100644 --- a/client/ws_client.go +++ b/client/ws_client.go @@ -85,6 +85,8 @@ func (wsc *WSClient) dial() error { func (wsc *WSClient) OnStop() { wsc.BaseService.OnStop() + wsc.Conn.Close() + wsc.Conn = nil // ResultsCh/ErrorsCh is closed in receiveEventsRoutine. } diff --git a/server/handlers.go b/server/handlers.go index ca42b2e67..5b6008504 100644 --- a/server/handlers.go +++ b/server/handlers.go @@ -373,7 +373,9 @@ func (wsc *wsConnection) OnStart() error { func (wsc *wsConnection) OnStop() { wsc.BaseService.OnStop() - wsc.evsw.RemoveListener(wsc.remoteAddr) + if wsc.evsw != nil { + wsc.evsw.RemoveListener(wsc.remoteAddr) + } wsc.readTimeout.Stop() wsc.pingTicker.Stop() // The write loop closes the websocket connection From d6587be7bcacf7cdc5a547f8a2f3862b3d7bd8df Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Tue, 21 Mar 2017 22:05:53 +0400 Subject: [PATCH 63/73] [WSClient] allow for multiple restarts needed for https://github.com/tendermint/tools/pull/13/commits/3044f66ba90694927fb22ea5267de2a90bb3281b See https://github.com/tendermint/tools/issues/6 --- client/ws_client.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/client/ws_client.go b/client/ws_client.go index b56547dd6..99737ca8f 100644 --- a/client/ws_client.go +++ b/client/ws_client.go @@ -53,6 +53,9 @@ func (wsc *WSClient) OnStart() error { if err != nil { return err } + + wsc.ResultsCh = make(chan json.RawMessage, wsResultsChannelCapacity) + wsc.ErrorsCh = make(chan error, wsErrorsChannelCapacity) go wsc.receiveEventsRoutine() return nil } From b0d2032488a16ce1497edd47e7c2e0dda246723f Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Tue, 28 Mar 2017 14:01:22 +0400 Subject: [PATCH 64/73] use BaseService.OnReset method to recreate channels --- client/ws_client.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/client/ws_client.go b/client/ws_client.go index 99737ca8f..fb359bdaf 100644 --- a/client/ws_client.go +++ b/client/ws_client.go @@ -47,16 +47,21 @@ func (wsc *WSClient) String() string { return wsc.Address + ", " + wsc.Endpoint } +// OnStart implements cmn.BaseService interface func (wsc *WSClient) OnStart() error { wsc.BaseService.OnStart() err := wsc.dial() if err != nil { return err } + go wsc.receiveEventsRoutine() + return nil +} +// OnReset implements cmn.BaseService interface +func (wsc *WSClient) OnReset() error { wsc.ResultsCh = make(chan json.RawMessage, wsResultsChannelCapacity) wsc.ErrorsCh = make(chan error, wsErrorsChannelCapacity) - go wsc.receiveEventsRoutine() return nil } @@ -86,6 +91,7 @@ func (wsc *WSClient) dial() error { return nil } +// OnStop implements cmn.BaseService interface func (wsc *WSClient) OnStop() { wsc.BaseService.OnStop() // ResultsCh/ErrorsCh is closed in receiveEventsRoutine. From ba5382b70e65f89cd771157fc900cfc6adb22819 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Tue, 28 Mar 2017 14:17:40 +0400 Subject: [PATCH 65/73] open result&error channels on start --- client/ws_client.go | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/client/ws_client.go b/client/ws_client.go index fb359bdaf..8fa6189d1 100644 --- a/client/ws_client.go +++ b/client/ws_client.go @@ -32,12 +32,10 @@ type WSClient struct { func NewWSClient(remoteAddr, endpoint string) *WSClient { addr, dialer := makeHTTPDialer(remoteAddr) wsClient := &WSClient{ - Address: addr, - Dialer: dialer, - Endpoint: endpoint, - Conn: nil, - ResultsCh: make(chan json.RawMessage, wsResultsChannelCapacity), - ErrorsCh: make(chan error, wsErrorsChannelCapacity), + Address: addr, + Dialer: dialer, + Endpoint: endpoint, + Conn: nil, } wsClient.BaseService = *cmn.NewBaseService(log, "WSClient", wsClient) return wsClient @@ -54,14 +52,14 @@ func (wsc *WSClient) OnStart() error { if err != nil { return err } + wsc.ResultsCh = make(chan json.RawMessage, wsResultsChannelCapacity) + wsc.ErrorsCh = make(chan error, wsErrorsChannelCapacity) go wsc.receiveEventsRoutine() return nil } // OnReset implements cmn.BaseService interface func (wsc *WSClient) OnReset() error { - wsc.ResultsCh = make(chan json.RawMessage, wsResultsChannelCapacity) - wsc.ErrorsCh = make(chan error, wsErrorsChannelCapacity) return nil } From 9d18cbe74e66f875afa36d2fa3be280e4a2dc9e6 Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Tue, 11 Apr 2017 13:29:49 +0200 Subject: [PATCH 66/73] Remove race condition between read go-routine and stop --- client/ws_client.go | 4 +++- server/handlers.go | 7 +++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/client/ws_client.go b/client/ws_client.go index 9ed2be8c5..9933ab073 100644 --- a/client/ws_client.go +++ b/client/ws_client.go @@ -86,7 +86,6 @@ func (wsc *WSClient) dial() error { func (wsc *WSClient) OnStop() { wsc.BaseService.OnStop() wsc.Conn.Close() - wsc.Conn = nil // ResultsCh/ErrorsCh is closed in receiveEventsRoutine. } @@ -112,6 +111,9 @@ func (wsc *WSClient) receiveEventsRoutine() { wsc.ResultsCh <- *response.Result } } + // this must be modified in the same go-routine that reads from the + // connection to avoid race conditions + wsc.Conn = nil // Cleanup close(wsc.ResultsCh) diff --git a/server/handlers.go b/server/handlers.go index 5b6008504..573696fb6 100644 --- a/server/handlers.go +++ b/server/handlers.go @@ -347,12 +347,15 @@ func NewWSConnection(baseConn *websocket.Conn, funcMap map[string]*RPCFunc, evsw func (wsc *wsConnection) OnStart() error { wsc.BaseService.OnStart() + // these must be set before the readRoutine is created, as it may + // call wsc.Stop(), which accesses these timers + wsc.readTimeout = time.NewTimer(time.Second * wsReadTimeoutSeconds) + wsc.pingTicker = time.NewTicker(time.Second * wsPingTickerSeconds) + // Read subscriptions/unsubscriptions to events go wsc.readRoutine() // Custom Ping handler to touch readTimeout - wsc.readTimeout = time.NewTimer(time.Second * wsReadTimeoutSeconds) - wsc.pingTicker = time.NewTicker(time.Second * wsPingTickerSeconds) wsc.baseConn.SetPingHandler(func(m string) error { // NOTE: https://github.com/gorilla/websocket/issues/97 go wsc.baseConn.WriteControl(websocket.PongMessage, []byte(m), time.Now().Add(time.Second*wsWriteTimeoutSeconds)) From c3295f4878019ff3fdfcac37a4c0e4bcf4bb02a7 Mon Sep 17 00:00:00 2001 From: Ethan Buchman Date: Wed, 12 Apr 2017 13:42:19 -0400 Subject: [PATCH 67/73] RPCRequest.Params can be map[string]interface{} or []interface{} --- server/handlers.go | 44 ++++++++++++++++++++++++++++++++------------ types/types.go | 8 ++++---- 2 files changed, 36 insertions(+), 16 deletions(-) diff --git a/server/handlers.go b/server/handlers.go index 573696fb6..051d67d00 100644 --- a/server/handlers.go +++ b/server/handlers.go @@ -140,36 +140,56 @@ func makeJSONRPCHandler(funcMap map[string]*RPCFunc) http.HandlerFunc { } } -// Convert a list of interfaces to properly typed values +// Convert a []interface{} OR a map[string]interface{} to properly typed values // // argsOffset is used in jsonParamsToArgsWS, where len(rpcFunc.args) != len(rpcFunc.argNames). // Example: // rpcFunc.args = [rpctypes.WSRPCContext string] // rpcFunc.argNames = ["arg"] -func jsonParamsToArgs(rpcFunc *RPCFunc, params map[string]interface{}, argsOffset int) ([]reflect.Value, error) { +func jsonParamsToArgs(rpcFunc *RPCFunc, paramsI interface{}, argsOffset int) ([]reflect.Value, error) { values := make([]reflect.Value, len(rpcFunc.argNames)) - for i, argName := range rpcFunc.argNames { - argType := rpcFunc.args[i+argsOffset] + switch params := paramsI.(type) { - // decode param if provided - if param, ok := params[argName]; ok && "" != param { - v, err := _jsonObjectToArg(argType, param) + case map[string]interface{}: + for i, argName := range rpcFunc.argNames { + argType := rpcFunc.args[i+argsOffset] + + // decode param if provided + if param, ok := params[argName]; ok && "" != param { + v, err := _jsonObjectToArg(argType, param) + if err != nil { + return nil, err + } + values[i] = v + } else { // use default for that type + values[i] = reflect.Zero(argType) + } + } + case []interface{}: + if len(rpcFunc.argNames) != len(params) { + return nil, errors.New(fmt.Sprintf("Expected %v parameters (%v), got %v (%v)", + len(rpcFunc.argNames), rpcFunc.argNames, len(params), params)) + } + values := make([]reflect.Value, len(params)) + for i, p := range params { + ty := rpcFunc.args[i] + v, err := _jsonObjectToArg(ty, p) if err != nil { return nil, err } values[i] = v - } else { // use default for that type - values[i] = reflect.Zero(argType) } + return values, nil + default: + return nil, fmt.Errorf("Unknown type for JSON params %v. Expected map[string]interface{} or []interface{}", reflect.TypeOf(paramsI)) } - return values, nil } // Same as above, but with the first param the websocket connection -func jsonParamsToArgsWS(rpcFunc *RPCFunc, params map[string]interface{}, wsCtx types.WSRPCContext) ([]reflect.Value, error) { - values, err := jsonParamsToArgs(rpcFunc, params, 1) +func jsonParamsToArgsWS(rpcFunc *RPCFunc, paramsI interface{}, wsCtx types.WSRPCContext) ([]reflect.Value, error) { + values, err := jsonParamsToArgs(rpcFunc, paramsI, 1) if err != nil { return nil, err } diff --git a/types/types.go b/types/types.go index cebd7564a..38c7f09db 100644 --- a/types/types.go +++ b/types/types.go @@ -9,10 +9,10 @@ import ( ) type RPCRequest struct { - JSONRPC string `json:"jsonrpc"` - ID string `json:"id"` - Method string `json:"method"` - Params map[string]interface{} `json:"params"` + JSONRPC string `json:"jsonrpc"` + ID string `json:"id"` + Method string `json:"method"` + Params interface{} `json:"params"` // must be map[string]interface{} or []interface{} } func NewRPCRequest(id string, method string, params map[string]interface{}) RPCRequest { From 8c385433576988ab4b0269a8220d095d82b96e6e Mon Sep 17 00:00:00 2001 From: Ethan Buchman Date: Wed, 12 Apr 2017 18:15:51 -0400 Subject: [PATCH 68/73] fix error msg --- server/handlers.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/handlers.go b/server/handlers.go index 051d67d00..de2b8094f 100644 --- a/server/handlers.go +++ b/server/handlers.go @@ -133,7 +133,7 @@ func makeJSONRPCHandler(funcMap map[string]*RPCFunc) http.HandlerFunc { log.Info("HTTPJSONRPC", "method", request.Method, "args", args, "returns", returns) result, err := unreflectResult(returns) if err != nil { - WriteRPCResponseHTTP(w, types.NewRPCResponse(request.ID, result, fmt.Sprintf("Error unreflecting result: %v", err.Error()))) + WriteRPCResponseHTTP(w, types.NewRPCResponse(request.ID, result, err.Error())) return } WriteRPCResponseHTTP(w, types.NewRPCResponse(request.ID, result, "")) @@ -231,7 +231,7 @@ func makeHTTPHandler(rpcFunc *RPCFunc) func(http.ResponseWriter, *http.Request) log.Info("HTTPRestRPC", "method", r.URL.Path, "args", args, "returns", returns) result, err := unreflectResult(returns) if err != nil { - WriteRPCResponseHTTP(w, types.NewRPCResponse("", nil, fmt.Sprintf("Error unreflecting result: %v", err.Error()))) + WriteRPCResponseHTTP(w, types.NewRPCResponse("", nil, err.Error())) return } WriteRPCResponseHTTP(w, types.NewRPCResponse("", result, "")) From 4b30cb3083f8275a8d9c94527c290996a42c3707 Mon Sep 17 00:00:00 2001 From: Ethan Buchman Date: Wed, 12 Apr 2017 19:30:05 -0400 Subject: [PATCH 69/73] test: check err on cmd.Wait --- rpc_test.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/rpc_test.go b/rpc_test.go index 8a05d7295..56b8ade32 100644 --- a/rpc_test.go +++ b/rpc_test.go @@ -71,7 +71,9 @@ func init() { if err != nil { panic(err) } - err = cmd.Wait() + if err = cmd.Wait(); err != nil { + panic(err) + } mux := http.NewServeMux() server.RegisterRPCFuncs(mux, Routes) From 1a42f946dc6bcd88f9f58c7f2fb86f785584d793 Mon Sep 17 00:00:00 2001 From: Ethan Buchman Date: Wed, 19 Apr 2017 00:05:18 -0400 Subject: [PATCH 70/73] version bump --- version.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.go b/version.go index 33eb7fe51..8828f260b 100644 --- a/version.go +++ b/version.go @@ -1,7 +1,7 @@ package rpc const Maj = "0" -const Min = "6" // 0x-prefixed string args handled as hex -const Fix = "0" // +const Min = "7" +const Fix = "0" const Version = Maj + "." + Min + "." + Fix From d6fd0c4ca07c0be6d6503d5231060301a3494ab1 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Fri, 21 Apr 2017 18:30:22 +0300 Subject: [PATCH 71/73] fix backward compatibility for WS --- rpc_test.go | 28 ++++++++++++++++++++++++++++ server/handlers.go | 2 +- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/rpc_test.go b/rpc_test.go index 56b8ade32..ed28cbc8d 100644 --- a/rpc_test.go +++ b/rpc_test.go @@ -261,6 +261,34 @@ func TestWSNewWSRPCFunc(t *testing.T) { } } +func TestWSHandlesArrayParams(t *testing.T) { + cl := client.NewWSClient(tcpAddr, websocketEndpoint) + _, err := cl.Start() + require.Nil(t, err) + defer cl.Stop() + + val := "acbd" + params := []interface{}{val} + err = cl.WriteJSON(types.RPCRequest{ + JSONRPC: "2.0", + ID: "", + Method: "echo_ws", + Params: params, + }) + require.Nil(t, err) + + select { + case msg := <-cl.ResultsCh: + result := new(Result) + wire.ReadJSONPtr(result, msg, &err) + require.Nil(t, err) + got := (*result).(*ResultEcho).Value + assert.Equal(t, got, val) + case err := <-cl.ErrorsCh: + t.Fatalf("%+v", err) + } +} + func randBytes(t *testing.T) []byte { n := rand.Intn(10) + 2 buf := make([]byte, n) diff --git a/server/handlers.go b/server/handlers.go index de2b8094f..9fa327cbb 100644 --- a/server/handlers.go +++ b/server/handlers.go @@ -173,7 +173,7 @@ func jsonParamsToArgs(rpcFunc *RPCFunc, paramsI interface{}, argsOffset int) ([] } values := make([]reflect.Value, len(params)) for i, p := range params { - ty := rpcFunc.args[i] + ty := rpcFunc.args[i+argsOffset] v, err := _jsonObjectToArg(ty, p) if err != nil { return nil, err From a01cff9ce630a93168ff7aa003072a7164bdc1ef Mon Sep 17 00:00:00 2001 From: Ethan Buchman Date: Fri, 21 Apr 2017 12:18:21 -0400 Subject: [PATCH 72/73] jsonParamsToArgsRPC func --- server/handlers.go | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/server/handlers.go b/server/handlers.go index 9fa327cbb..9be64b775 100644 --- a/server/handlers.go +++ b/server/handlers.go @@ -124,7 +124,7 @@ func makeJSONRPCHandler(funcMap map[string]*RPCFunc) http.HandlerFunc { WriteRPCResponseHTTP(w, types.NewRPCResponse(request.ID, nil, "RPC method is only for websockets: "+request.Method)) return } - args, err := jsonParamsToArgs(rpcFunc, request.Params, 0) + args, err := jsonParamsToArgsRPC(rpcFunc, request.Params) if err != nil { WriteRPCResponseHTTP(w, types.NewRPCResponse(request.ID, nil, fmt.Sprintf("Error converting json params to arguments: %v", err.Error()))) return @@ -142,7 +142,7 @@ func makeJSONRPCHandler(funcMap map[string]*RPCFunc) http.HandlerFunc { // Convert a []interface{} OR a map[string]interface{} to properly typed values // -// argsOffset is used in jsonParamsToArgsWS, where len(rpcFunc.args) != len(rpcFunc.argNames). +// argsOffset should be 0 for RPC calls, and 1 for WS requests, where len(rpcFunc.args) != len(rpcFunc.argNames). // Example: // rpcFunc.args = [rpctypes.WSRPCContext string] // rpcFunc.argNames = ["arg"] @@ -187,6 +187,11 @@ func jsonParamsToArgs(rpcFunc *RPCFunc, paramsI interface{}, argsOffset int) ([] return values, nil } +// Convert a []interface{} OR a map[string]interface{} to properly typed values +func jsonParamsToArgsRPC(rpcFunc *RPCFunc, paramsI interface{}) ([]reflect.Value, error) { + return jsonParamsToArgs(rpcFunc, paramsI, 0) +} + // Same as above, but with the first param the websocket connection func jsonParamsToArgsWS(rpcFunc *RPCFunc, paramsI interface{}, wsCtx types.WSRPCContext) ([]reflect.Value, error) { values, err := jsonParamsToArgs(rpcFunc, paramsI, 1) @@ -494,7 +499,7 @@ func (wsc *wsConnection) readRoutine() { wsCtx := types.WSRPCContext{Request: request, WSRPCConnection: wsc} args, err = jsonParamsToArgsWS(rpcFunc, request.Params, wsCtx) } else { - args, err = jsonParamsToArgs(rpcFunc, request.Params, 0) + args, err = jsonParamsToArgsRPC(rpcFunc, request.Params) } if err != nil { wsc.WriteRPCResponse(types.NewRPCResponse(request.ID, nil, err.Error())) From 15d5b2ac497da95cd2dceb9c087910ccec4dacb2 Mon Sep 17 00:00:00 2001 From: Ethan Buchman Date: Fri, 21 Apr 2017 17:51:11 -0400 Subject: [PATCH 73/73] use tmlibs --- client/ws_client.go | 2 +- server/handlers.go | 4 ++-- test/main.go | 2 +- types/types.go | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/client/ws_client.go b/client/ws_client.go index 16dc474c3..b83d62974 100644 --- a/client/ws_client.go +++ b/client/ws_client.go @@ -8,7 +8,7 @@ import ( "github.com/gorilla/websocket" "github.com/pkg/errors" - cmn "github.com/tendermint/go-common" + cmn "github.com/tendermint/tmlibs/common" types "github.com/tendermint/go-rpc/types" wire "github.com/tendermint/go-wire" ) diff --git a/server/handlers.go b/server/handlers.go index 9be64b775..456b4aaf5 100644 --- a/server/handlers.go +++ b/server/handlers.go @@ -14,10 +14,10 @@ import ( "github.com/gorilla/websocket" "github.com/pkg/errors" - cmn "github.com/tendermint/go-common" - events "github.com/tendermint/go-events" types "github.com/tendermint/go-rpc/types" wire "github.com/tendermint/go-wire" + cmn "github.com/tendermint/tmlibs/common" + events "github.com/tendermint/tmlibs/events" ) // Adds a route for each function in the funcMap, as well as general jsonrpc and websocket handlers for all functions. diff --git a/test/main.go b/test/main.go index 28de2be88..59c6e9b8f 100644 --- a/test/main.go +++ b/test/main.go @@ -4,7 +4,7 @@ import ( "fmt" "net/http" - cmn "github.com/tendermint/go-common" + cmn "github.com/tendermint/tmlibs/common" rpcserver "github.com/tendermint/go-rpc/server" ) diff --git a/types/types.go b/types/types.go index 38c7f09db..9c5f2625b 100644 --- a/types/types.go +++ b/types/types.go @@ -4,8 +4,8 @@ import ( "encoding/json" "strings" - events "github.com/tendermint/go-events" wire "github.com/tendermint/go-wire" + events "github.com/tendermint/tmlibs/events" ) type RPCRequest struct {