@ -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 |
@ -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. |
@ -0,0 +1,18 @@ | |||
PACKAGES=$(shell go list ./... | grep -v "test") | |||
all: get_deps test | |||
test: | |||
@echo "--> Running go test --race" | |||
@go test --race $(PACKAGES) | |||
@echo "--> Running integration tests" | |||
@bash ./test/integration_test.sh | |||
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 |
@ -0,0 +1,128 @@ | |||
# 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 | |||
# 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": { | |||
"name": "my_world", | |||
"num": 5 | |||
} | |||
} | |||
``` | |||
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: | |||
``` | |||
// 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:8008", mux) | |||
if err != nil { | |||
panic(err) | |||
} | |||
}() | |||
``` | |||
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. | |||
# Examples | |||
* [Tendermint](https://github.com/tendermint/tendermint/blob/master/rpc/core/routes.go) | |||
* [tm-monitor](https://github.com/tendermint/tools/blob/master/tm-monitor/rpc.go) | |||
## CHANGELOG | |||
### 0.7.0 | |||
BREAKING CHANGES: | |||
- removed `Client` empty interface | |||
- `ClientJSONRPC#Call` `params` argument became a map | |||
- rename `ClientURI` -> `URIClient`, `ClientJSONRPC` -> `JSONRPCClient` | |||
IMPROVEMENTS: | |||
- added `HTTPClient` interface, which can be used for both `ClientURI` | |||
and `ClientJSONRPC` | |||
- all params are now optional (Golang's default will be used if some param is missing) | |||
- added `Call` method to `WSClient` (see method's doc for details) |
@ -0,0 +1,21 @@ | |||
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 | |||
dependencies: | |||
override: | |||
- "cd $REPO && make get_deps" | |||
test: | |||
override: | |||
- "cd $REPO && make test" |
@ -0,0 +1,198 @@ | |||
package rpcclient | |||
import ( | |||
"bytes" | |||
"encoding/json" | |||
"fmt" | |||
"io/ioutil" | |||
"net" | |||
"net/http" | |||
"net/url" | |||
"reflect" | |||
"strings" | |||
"github.com/pkg/errors" | |||
types "github.com/tendermint/go-rpc/types" | |||
wire "github.com/tendermint/go-wire" | |||
) | |||
// HTTPClient is a common interface for JSONRPCClient and URIClient. | |||
type HTTPClient interface { | |||
Call(method string, params map[string]interface{}, result interface{}) (interface{}, error) | |||
} | |||
// TODO: Deprecate support for IP:PORT or /path/to/socket | |||
func makeHTTPDialer(remoteAddr string) (string, func(string, string) (net.Conn, error)) { | |||
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 = types.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) | |||
} | |||
} | |||
// 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, | |||
}, | |||
} | |||
} | |||
//------------------------------------------------------------------------------------ | |||
// JSON rpc takes params as a slice | |||
type JSONRPCClient struct { | |||
address string | |||
client *http.Client | |||
} | |||
func NewJSONRPCClient(remote string) *JSONRPCClient { | |||
address, client := makeHTTPClient(remote) | |||
return &JSONRPCClient{ | |||
address: address, | |||
client: client, | |||
} | |||
} | |||
func (c *JSONRPCClient) Call(method string, params map[string]interface{}, result interface{}) (interface{}, error) { | |||
// we need this step because we attempt to decode values using `go-wire` | |||
// (handlers.go:176) on the server side | |||
encodedParams := make(map[string]interface{}) | |||
for k, v := range params { | |||
bytes := json.RawMessage(wire.JSONBytes(v)) | |||
encodedParams[k] = &bytes | |||
} | |||
request := types.RPCRequest{ | |||
JSONRPC: "2.0", | |||
Method: method, | |||
Params: encodedParams, | |||
ID: "", | |||
} | |||
requestBytes, err := json.Marshal(request) | |||
if err != nil { | |||
return nil, err | |||
} | |||
// log.Info(string(requestBytes)) | |||
requestBuf := bytes.NewBuffer(requestBytes) | |||
// log.Info(Fmt("RPC request to %v (%v): %v", c.remote, method, string(requestBytes))) | |||
httpResponse, err := c.client.Post(c.address, "text/json", requestBuf) | |||
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, result) | |||
} | |||
//------------------------------------------------------------- | |||
// URI takes params as a map | |||
type URIClient struct { | |||
address string | |||
client *http.Client | |||
} | |||
func NewURIClient(remote string) *URIClient { | |||
address, client := makeHTTPClient(remote) | |||
return &URIClient{ | |||
address: address, | |||
client: client, | |||
} | |||
} | |||
func (c *URIClient) 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", c.address, method, values)) | |||
resp, err := c.client.PostForm(c.address+"/"+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, result) | |||
} | |||
//------------------------------------------------ | |||
func unmarshalResponseBytes(responseBytes []byte, result interface{}) (interface{}, error) { | |||
// 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 := &types.RPCResponse{} | |||
err = json.Unmarshal(responseBytes, response) | |||
if err != nil { | |||
return nil, errors.Errorf("Error unmarshalling rpc response: %v", err) | |||
} | |||
errorStr := response.Error | |||
if errorStr != "" { | |||
return nil, errors.Errorf("Response error: %v", errorStr) | |||
} | |||
// unmarshal the RawMessage into the result | |||
result = wire.ReadJSONPtr(result, *response.Result, &err) | |||
if err != nil { | |||
return nil, errors.Errorf("Error unmarshalling rpc response result: %v", err) | |||
} | |||
return result, nil | |||
} | |||
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 { | |||
// 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 { | |||
return err | |||
} | |||
args[k] = buf.String() | |||
} | |||
return nil | |||
} |
@ -0,0 +1,7 @@ | |||
package rpcclient | |||
import ( | |||
"github.com/tendermint/log15" | |||
) | |||
var log = log15.New("module", "rpcclient") |
@ -0,0 +1,172 @@ | |||
package rpcclient | |||
import ( | |||
"encoding/json" | |||
"net" | |||
"net/http" | |||
"time" | |||
"github.com/gorilla/websocket" | |||
"github.com/pkg/errors" | |||
cmn "github.com/tendermint/tmlibs/common" | |||
types "github.com/tendermint/go-rpc/types" | |||
wire "github.com/tendermint/go-wire" | |||
) | |||
const ( | |||
wsResultsChannelCapacity = 10 | |||
wsErrorsChannelCapacity = 1 | |||
wsWriteTimeoutSeconds = 10 | |||
) | |||
type WSClient struct { | |||
cmn.BaseService | |||
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(remoteAddr, endpoint string) *WSClient { | |||
addr, dialer := makeHTTPDialer(remoteAddr) | |||
wsClient := &WSClient{ | |||
Address: addr, | |||
Dialer: dialer, | |||
Endpoint: endpoint, | |||
Conn: nil, | |||
} | |||
wsClient.BaseService = *cmn.NewBaseService(log, "WSClient", wsClient) | |||
return wsClient | |||
} | |||
func (wsc *WSClient) String() string { | |||
return wsc.Address + ", " + wsc.Endpoint | |||
} | |||
// OnStart implements cmn.BaseService interface | |||
func (wsc *WSClient) OnStart() error { | |||
wsc.BaseService.OnStart() | |||
err := wsc.dial() | |||
if err != nil { | |||
return err | |||
} | |||
wsc.ResultsCh = make(chan json.RawMessage, wsResultsChannelCapacity) | |||
wsc.ErrorsCh = make(chan error, wsErrorsChannelCapacity) | |||
go wsc.receiveEventsRoutine() | |||
return nil | |||
} | |||
// OnReset implements cmn.BaseService interface | |||
func (wsc *WSClient) OnReset() error { | |||
return nil | |||
} | |||
func (wsc *WSClient) dial() error { | |||
// Dial | |||
dialer := &websocket.Dialer{ | |||
NetDial: wsc.Dialer, | |||
Proxy: http.ProxyFromEnvironment, | |||
} | |||
rHeader := http.Header{} | |||
con, _, err := dialer.Dial("ws://"+wsc.Address+wsc.Endpoint, 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 | |||
go con.WriteControl(websocket.PongMessage, []byte(m), time.Now().Add(time.Second*wsWriteTimeoutSeconds)) | |||
return nil | |||
}) | |||
con.SetPongHandler(func(m string) error { | |||
// NOTE: https://github.com/gorilla/websocket/issues/97 | |||
return nil | |||
}) | |||
wsc.Conn = con | |||
return nil | |||
} | |||
// OnStop implements cmn.BaseService interface | |||
func (wsc *WSClient) OnStop() { | |||
wsc.BaseService.OnStop() | |||
wsc.Conn.Close() | |||
// ResultsCh/ErrorsCh is closed in receiveEventsRoutine. | |||
} | |||
func (wsc *WSClient) receiveEventsRoutine() { | |||
for { | |||
_, 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 types.RPCResponse | |||
err := json.Unmarshal(data, &response) | |||
if err != nil { | |||
log.Info("WSClient failed to parse message", "error", err, "data", string(data)) | |||
wsc.ErrorsCh <- err | |||
continue | |||
} | |||
if response.Error != "" { | |||
wsc.ErrorsCh <- errors.Errorf(response.Error) | |||
continue | |||
} | |||
wsc.ResultsCh <- *response.Result | |||
} | |||
} | |||
// this must be modified in the same go-routine that reads from the | |||
// connection to avoid race conditions | |||
wsc.Conn = nil | |||
// Cleanup | |||
close(wsc.ResultsCh) | |||
close(wsc.ErrorsCh) | |||
} | |||
// Subscribe to an event. Note the server must have a "subscribe" route | |||
// defined. | |||
func (wsc *WSClient) Subscribe(eventid string) error { | |||
err := wsc.WriteJSON(types.RPCRequest{ | |||
JSONRPC: "2.0", | |||
ID: "", | |||
Method: "subscribe", | |||
Params: map[string]interface{}{"event": eventid}, | |||
}) | |||
return err | |||
} | |||
// 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", | |||
ID: "", | |||
Method: "unsubscribe", | |||
Params: map[string]interface{}{"event": eventid}, | |||
}) | |||
return err | |||
} | |||
// Call asynchronously calls a given method by sending an RPCRequest to the | |||
// server. Results will be available on ResultsCh, errors, if any, on ErrorsCh. | |||
func (wsc *WSClient) Call(method string, params map[string]interface{}) error { | |||
// we need this step because we attempt to decode values using `go-wire` | |||
// (handlers.go:470) on the server side | |||
encodedParams := make(map[string]interface{}) | |||
for k, v := range params { | |||
bytes := json.RawMessage(wire.JSONBytes(v)) | |||
encodedParams[k] = &bytes | |||
} | |||
err := wsc.WriteJSON(types.RPCRequest{ | |||
JSONRPC: "2.0", | |||
Method: method, | |||
Params: encodedParams, | |||
ID: "", | |||
}) | |||
return err | |||
} |
@ -0,0 +1,298 @@ | |||
package rpc | |||
import ( | |||
"bytes" | |||
crand "crypto/rand" | |||
"fmt" | |||
"math/rand" | |||
"net/http" | |||
"os/exec" | |||
"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" | |||
wire "github.com/tendermint/go-wire" | |||
) | |||
// Client and Server should work over tcp or unix sockets | |||
const ( | |||
tcpAddr = "tcp://0.0.0.0:46657" | |||
unixSocket = "/tmp/go-rpc.sock" | |||
unixAddr = "unix:///tmp/go-rpc.sock" | |||
websocketEndpoint = "/websocket/endpoint" | |||
) | |||
// Define a type for results and register concrete versions | |||
type Result interface{} | |||
type ResultEcho struct { | |||
Value string | |||
} | |||
type ResultEchoBytes struct { | |||
Value []byte | |||
} | |||
var _ = wire.RegisterInterface( | |||
struct{ Result }{}, | |||
wire.ConcreteType{&ResultEcho{}, 0x1}, | |||
wire.ConcreteType{&ResultEchoBytes{}, 0x2}, | |||
) | |||
// Define some routes | |||
var Routes = map[string]*server.RPCFunc{ | |||
"echo": server.NewRPCFunc(EchoResult, "arg"), | |||
"echo_ws": server.NewWSRPCFunc(EchoWSResult, "arg"), | |||
"echo_bytes": server.NewRPCFunc(EchoBytesResult, "arg"), | |||
} | |||
func EchoResult(v string) (Result, error) { | |||
return &ResultEcho{v}, nil | |||
} | |||
func EchoWSResult(wsCtx types.WSRPCContext, v string) (Result, error) { | |||
return &ResultEcho{v}, nil | |||
} | |||
func EchoBytesResult(v []byte) (Result, error) { | |||
return &ResultEchoBytes{v}, nil | |||
} | |||
// launch unix and tcp servers | |||
func init() { | |||
cmd := exec.Command("rm", "-f", unixSocket) | |||
err := cmd.Start() | |||
if err != nil { | |||
panic(err) | |||
} | |||
if err = cmd.Wait(); err != nil { | |||
panic(err) | |||
} | |||
mux := http.NewServeMux() | |||
server.RegisterRPCFuncs(mux, Routes) | |||
wm := server.NewWebsocketManager(Routes, nil) | |||
mux.HandleFunc(websocketEndpoint, wm.WebsocketHandler) | |||
go func() { | |||
_, err := server.StartHTTPServer(tcpAddr, mux) | |||
if err != nil { | |||
panic(err) | |||
} | |||
}() | |||
mux2 := http.NewServeMux() | |||
server.RegisterRPCFuncs(mux2, Routes) | |||
wm = server.NewWebsocketManager(Routes, nil) | |||
mux2.HandleFunc(websocketEndpoint, wm.WebsocketHandler) | |||
go func() { | |||
_, err := server.StartHTTPServer(unixAddr, mux2) | |||
if err != nil { | |||
panic(err) | |||
} | |||
}() | |||
// wait for servers to start | |||
time.Sleep(time.Second * 2) | |||
} | |||
func echoViaHTTP(cl client.HTTPClient, val string) (string, error) { | |||
params := map[string]interface{}{ | |||
"arg": val, | |||
} | |||
var result Result | |||
if _, err := cl.Call("echo", params, &result); err != nil { | |||
return "", err | |||
} | |||
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 := echoViaHTTP(cl, val) | |||
require.Nil(t, err) | |||
assert.Equal(t, got, val) | |||
val2 := randBytes(t) | |||
got2, err := echoBytesViaHTTP(cl, val2) | |||
require.Nil(t, err) | |||
assert.Equal(t, got2, val2) | |||
} | |||
func echoViaWS(cl *client.WSClient, val string) (string, error) { | |||
params := map[string]interface{}{ | |||
"arg": val, | |||
} | |||
err := cl.Call("echo", params) | |||
if err != nil { | |||
return "", err | |||
} | |||
select { | |||
case msg := <-cl.ResultsCh: | |||
result := new(Result) | |||
wire.ReadJSONPtr(result, msg, &err) | |||
if err != nil { | |||
return "", nil | |||
} | |||
return (*result).(*ResultEcho).Value, nil | |||
case err := <-cl.ErrorsCh: | |||
return "", err | |||
} | |||
} | |||
func echoBytesViaWS(cl *client.WSClient, bytes []byte) ([]byte, error) { | |||
params := map[string]interface{}{ | |||
"arg": bytes, | |||
} | |||
err := cl.Call("echo_bytes", 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) { | |||
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) | |||
cl2 := client.NewJSONRPCClient(tcpAddr) | |||
fmt.Printf("=== testing server on %s using %v client", addr, cl2) | |||
testWithHTTPClient(t, cl2) | |||
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" | |||
got, err := echoViaHTTP(cl, val) | |||
require.Nil(t, err) | |||
assert.Equal(t, got, val) | |||
} | |||
func TestQuotedStringArg(t *testing.T) { | |||
cl := client.NewURIClient(tcpAddr) | |||
// should NOT be unquoted | |||
val := "\"abc\"" | |||
got, err := echoViaHTTP(cl, val) | |||
require.Nil(t, err) | |||
assert.Equal(t, got, val) | |||
} | |||
func TestWSNewWSRPCFunc(t *testing.T) { | |||
cl := client.NewWSClient(tcpAddr, websocketEndpoint) | |||
_, err := cl.Start() | |||
require.Nil(t, err) | |||
defer cl.Stop() | |||
val := "acbd" | |||
params := map[string]interface{}{ | |||
"arg": val, | |||
} | |||
err = cl.WriteJSON(types.RPCRequest{ | |||
JSONRPC: "2.0", | |||
ID: "", | |||
Method: "echo_ws", | |||
Params: params, | |||
}) | |||
require.Nil(t, err) | |||
select { | |||
case msg := <-cl.ResultsCh: | |||
result := new(Result) | |||
wire.ReadJSONPtr(result, msg, &err) | |||
require.Nil(t, err) | |||
got := (*result).(*ResultEcho).Value | |||
assert.Equal(t, got, val) | |||
case err := <-cl.ErrorsCh: | |||
t.Fatal(err) | |||
} | |||
} | |||
func TestWSHandlesArrayParams(t *testing.T) { | |||
cl := client.NewWSClient(tcpAddr, websocketEndpoint) | |||
_, err := cl.Start() | |||
require.Nil(t, err) | |||
defer cl.Stop() | |||
val := "acbd" | |||
params := []interface{}{val} | |||
err = cl.WriteJSON(types.RPCRequest{ | |||
JSONRPC: "2.0", | |||
ID: "", | |||
Method: "echo_ws", | |||
Params: params, | |||
}) | |||
require.Nil(t, err) | |||
select { | |||
case msg := <-cl.ResultsCh: | |||
result := new(Result) | |||
wire.ReadJSONPtr(result, msg, &err) | |||
require.Nil(t, err) | |||
got := (*result).(*ResultEcho).Value | |||
assert.Equal(t, got, val) | |||
case err := <-cl.ErrorsCh: | |||
t.Fatalf("%+v", err) | |||
} | |||
} | |||
func randBytes(t *testing.T) []byte { | |||
n := rand.Intn(10) + 2 | |||
buf := make([]byte, n) | |||
_, err := crand.Read(buf) | |||
require.Nil(t, err) | |||
return bytes.Replace(buf, []byte("="), []byte{100}, -1) | |||
} |
@ -0,0 +1,649 @@ | |||
package rpcserver | |||
import ( | |||
"bytes" | |||
"encoding/hex" | |||
"encoding/json" | |||
"fmt" | |||
"io/ioutil" | |||
"net/http" | |||
"reflect" | |||
"sort" | |||
"strings" | |||
"time" | |||
"github.com/gorilla/websocket" | |||
"github.com/pkg/errors" | |||
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. | |||
// "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 { | |||
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 | |||
// 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 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: argNames, | |||
ws: ws, | |||
} | |||
} | |||
// 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 types.RPCRequest | |||
err := json.Unmarshal(b, &request) | |||
if err != nil { | |||
WriteRPCResponseHTTP(w, types.NewRPCResponse("", nil, fmt.Sprintf("Error unmarshalling request: %v", err.Error()))) | |||
return | |||
} | |||
if len(r.URL.Path) > 1 { | |||
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, types.NewRPCResponse(request.ID, nil, "RPC method unknown: "+request.Method)) | |||
return | |||
} | |||
if rpcFunc.ws { | |||
WriteRPCResponseHTTP(w, types.NewRPCResponse(request.ID, nil, "RPC method is only for websockets: "+request.Method)) | |||
return | |||
} | |||
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 | |||
} | |||
returns := rpcFunc.f.Call(args) | |||
log.Info("HTTPJSONRPC", "method", request.Method, "args", args, "returns", returns) | |||
result, err := unreflectResult(returns) | |||
if err != nil { | |||
WriteRPCResponseHTTP(w, types.NewRPCResponse(request.ID, result, err.Error())) | |||
return | |||
} | |||
WriteRPCResponseHTTP(w, types.NewRPCResponse(request.ID, result, "")) | |||
} | |||
} | |||
// Convert a []interface{} OR a map[string]interface{} to properly typed values | |||
// | |||
// argsOffset should be 0 for RPC calls, and 1 for WS requests, where len(rpcFunc.args) != len(rpcFunc.argNames). | |||
// Example: | |||
// rpcFunc.args = [rpctypes.WSRPCContext string] | |||
// rpcFunc.argNames = ["arg"] | |||
func jsonParamsToArgs(rpcFunc *RPCFunc, paramsI interface{}, argsOffset int) ([]reflect.Value, error) { | |||
values := make([]reflect.Value, len(rpcFunc.argNames)) | |||
switch params := paramsI.(type) { | |||
case map[string]interface{}: | |||
for i, argName := range rpcFunc.argNames { | |||
argType := rpcFunc.args[i+argsOffset] | |||
// decode param if provided | |||
if param, ok := params[argName]; ok && "" != param { | |||
v, err := _jsonObjectToArg(argType, param) | |||
if err != nil { | |||
return nil, err | |||
} | |||
values[i] = v | |||
} else { // use default for that type | |||
values[i] = reflect.Zero(argType) | |||
} | |||
} | |||
case []interface{}: | |||
if len(rpcFunc.argNames) != len(params) { | |||
return nil, errors.New(fmt.Sprintf("Expected %v parameters (%v), got %v (%v)", | |||
len(rpcFunc.argNames), rpcFunc.argNames, len(params), params)) | |||
} | |||
values := make([]reflect.Value, len(params)) | |||
for i, p := range params { | |||
ty := rpcFunc.args[i+argsOffset] | |||
v, err := _jsonObjectToArg(ty, p) | |||
if err != nil { | |||
return nil, err | |||
} | |||
values[i] = v | |||
} | |||
return values, nil | |||
default: | |||
return nil, fmt.Errorf("Unknown type for JSON params %v. Expected map[string]interface{} or []interface{}", reflect.TypeOf(paramsI)) | |||
} | |||
return values, nil | |||
} | |||
// Convert a []interface{} OR a map[string]interface{} to properly typed values | |||
func jsonParamsToArgsRPC(rpcFunc *RPCFunc, paramsI interface{}) ([]reflect.Value, error) { | |||
return jsonParamsToArgs(rpcFunc, paramsI, 0) | |||
} | |||
// Same as above, but with the first param the websocket connection | |||
func jsonParamsToArgsWS(rpcFunc *RPCFunc, paramsI interface{}, wsCtx types.WSRPCContext) ([]reflect.Value, error) { | |||
values, err := jsonParamsToArgs(rpcFunc, paramsI, 1) | |||
if err != nil { | |||
return nil, err | |||
} | |||
return append([]reflect.Value{reflect.ValueOf(wsCtx)}, 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, types.NewRPCResponse("", nil, "This RPC method is only for websockets")) | |||
} | |||
} | |||
// 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, 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, types.NewRPCResponse("", nil, err.Error())) | |||
return | |||
} | |||
WriteRPCResponseHTTP(w, types.NewRPCResponse("", result, "")) | |||
} | |||
} | |||
// Covert an http query to a list of properly typed values. | |||
// To be properly decoded the arg must be a concrete type from tendermint (if its an interface). | |||
func httpParamsToArgs(rpcFunc *RPCFunc, r *http.Request) ([]reflect.Value, error) { | |||
values := make([]reflect.Value, len(rpcFunc.args)) | |||
for i, name := range rpcFunc.argNames { | |||
argType := rpcFunc.args[i] | |||
values[i] = reflect.Zero(argType) // set default for that type | |||
arg := GetParam(r, name) | |||
// log.Notice("param to arg", "argType", argType, "name", name, "arg", arg) | |||
if "" == arg { | |||
continue | |||
} | |||
v, err, ok := nonJsonToArg(argType, arg) | |||
if err != nil { | |||
return nil, err | |||
} | |||
if ok { | |||
values[i] = v | |||
continue | |||
} | |||
// Pass values to go-wire | |||
values[i], err = _jsonStringToArg(argType, 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 | |||
} | |||
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 := errors.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 | |||
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 { | |||
cmn.BaseService | |||
remoteAddr string | |||
baseConn *websocket.Conn | |||
writeChan chan types.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 types.RPCResponse, writeChanCapacity), // error when full. | |||
funcMap: funcMap, | |||
evsw: evsw, | |||
} | |||
wsc.BaseService = *cmn.NewBaseService(log, "wsConnection", wsc) | |||
return wsc | |||
} | |||
// wsc.Start() blocks until the connection closes. | |||
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.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.BaseService.OnStop() | |||
if wsc.evsw != nil { | |||
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. | |||
// Goroutine-safe | |||
func (wsc *wsConnection) WriteRPCResponse(resp types.RPCResponse) { | |||
select { | |||
case <-wsc.Quit: | |||
return | |||
case wsc.writeChan <- resp: | |||
} | |||
} | |||
// Implements WSRPCConnection | |||
// Nonblocking write. | |||
// Goroutine-safe | |||
func (wsc *wsConnection) TryWriteRPCResponse(resp types.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, "err", err.Error()) | |||
// an error reading the connection, | |||
// kill the connection | |||
wsc.Stop() | |||
return | |||
} | |||
var request types.RPCRequest | |||
err = json.Unmarshal(in, &request) | |||
if err != nil { | |||
errStr := fmt.Sprintf("Error unmarshaling data: %s", err.Error()) | |||
wsc.WriteRPCResponse(types.NewRPCResponse(request.ID, nil, errStr)) | |||
continue | |||
} | |||
// Now, fetch the RPCFunc and execute it. | |||
rpcFunc := wsc.funcMap[request.Method] | |||
if rpcFunc == nil { | |||
wsc.WriteRPCResponse(types.NewRPCResponse(request.ID, nil, "RPC method unknown: "+request.Method)) | |||
continue | |||
} | |||
var args []reflect.Value | |||
if rpcFunc.ws { | |||
wsCtx := types.WSRPCContext{Request: request, WSRPCConnection: wsc} | |||
args, err = jsonParamsToArgsWS(rpcFunc, request.Params, wsCtx) | |||
} else { | |||
args, err = jsonParamsToArgsRPC(rpcFunc, request.Params) | |||
} | |||
if err != nil { | |||
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(types.NewRPCResponse(request.ID, nil, err.Error())) | |||
continue | |||
} else { | |||
wsc.WriteRPCResponse(types.NewRPCResponse(request.ID, result, "")) | |||
continue | |||
} | |||
} | |||
} | |||
} | |||
// receives on a write channel and writes out on the socket | |||
func (wsc *wsConnection) writeRoutine() { | |||
defer wsc.baseConn.Close() | |||
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: | |||
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)) | |||
if err = wsc.baseConn.WriteMessage(websocket.TextMessage, jsonBytes); 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 | |||
//----------------------------------------------------------------------------- | |||
// 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, errors.Errorf("%v", errV.Interface()) | |||
} | |||
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 | |||
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("<html><body>") | |||
buf.WriteString("<br>Available endpoints:<br>") | |||
for _, name := range noArgNames { | |||
link := fmt.Sprintf("http://%s/%s", r.Host, name) | |||
buf.WriteString(fmt.Sprintf("<a href=\"%s\">%s</a></br>", link, link)) | |||
} | |||
buf.WriteString("<br>Endpoints that require arguments:<br>") | |||
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("<a href=\"%s\">%s</a></br>", link, link)) | |||
} | |||
buf.WriteString("</body></html>") | |||
w.Header().Set("Content-Type", "text/html") | |||
w.WriteHeader(200) | |||
w.Write(buf.Bytes()) | |||
} |
@ -0,0 +1,90 @@ | |||
package rpcserver | |||
import ( | |||
"encoding/hex" | |||
"net/http" | |||
"regexp" | |||
"strconv" | |||
"github.com/pkg/errors" | |||
) | |||
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, errors.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, errors.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, errors.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, errors.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 "", errors.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, errors.Errorf(param, err.Error()) | |||
} | |||
return f, nil | |||
} |
@ -0,0 +1,125 @@ | |||
// Commons for HTTP handling | |||
package rpcserver | |||
import ( | |||
"bufio" | |||
"encoding/json" | |||
"fmt" | |||
"net" | |||
"net/http" | |||
"runtime/debug" | |||
"strings" | |||
"time" | |||
"github.com/pkg/errors" | |||
types "github.com/tendermint/go-rpc/types" | |||
) | |||
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 = types.SocketType(listenAddr) | |||
addr = listenAddr | |||
// return nil, errors.Errorf("Invalid listener address %s", lisenAddr) | |||
} else { | |||
proto, addr = parts[0], parts[1] | |||
} | |||
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, errors.Errorf("Failed to listen to %v: %v", listenAddr, err) | |||
} | |||
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 types.RPCResponse) { | |||
// 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) | |||
} | |||
//----------------------------------------------------------------------------- | |||
// 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.(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, types.NewRPCResponse("", nil, fmt.Sprintf("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() | |||
} |
@ -0,0 +1,7 @@ | |||
package rpcserver | |||
import ( | |||
"github.com/tendermint/log15" | |||
) | |||
var log = log15.New("module", "rpcserver") |
@ -0,0 +1,9 @@ | |||
{ | |||
"jsonrpc": "2.0", | |||
"id": "", | |||
"method": "hello_world", | |||
"params": { | |||
"name": "my_world", | |||
"num": 5 | |||
} | |||
} |
@ -0,0 +1,95 @@ | |||
#!/usr/bin/env bash | |||
set -e | |||
# Get the directory of where this script is. | |||
SOURCE="${BASH_SOURCE[0]}" | |||
while [ -h "$SOURCE" ] ; do SOURCE="$(readlink "$SOURCE")"; done | |||
DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )" | |||
# Change into that dir because we expect that. | |||
pushd "$DIR" | |||
echo "==> Building the server" | |||
go build -o rpcserver main.go | |||
echo "==> (Re)starting the server" | |||
PID=$(pgrep rpcserver || echo "") | |||
if [[ $PID != "" ]]; then | |||
kill -9 "$PID" | |||
fi | |||
./rpcserver & | |||
PID=$! | |||
sleep 2 | |||
echo "==> simple request" | |||
R1=$(curl -s 'http://localhost:8008/hello_world?name="my_world"&num=5') | |||
R2=$(curl -s --data @data.json http://localhost:8008) | |||
if [[ "$R1" != "$R2" ]]; then | |||
echo "responses are not identical:" | |||
echo "R1: $R1" | |||
echo "R2: $R2" | |||
echo "FAIL" | |||
exit 1 | |||
else | |||
echo "OK" | |||
fi | |||
echo "==> request with 0x-prefixed hex string arg" | |||
R1=$(curl -s 'http://localhost:8008/hello_world?name=0x41424344&num=123') | |||
R2='{"jsonrpc":"2.0","id":"","result":{"Result":"hi ABCD 123"},"error":""}' | |||
if [[ "$R1" != "$R2" ]]; then | |||
echo "responses are not identical:" | |||
echo "R1: $R1" | |||
echo "R2: $R2" | |||
echo "FAIL" | |||
exit 1 | |||
else | |||
echo "OK" | |||
fi | |||
echo "==> request with missing params" | |||
R1=$(curl -s 'http://localhost:8008/hello_world') | |||
R2='{"jsonrpc":"2.0","id":"","result":{"Result":"hi 0"},"error":""}' | |||
if [[ "$R1" != "$R2" ]]; then | |||
echo "responses are not identical:" | |||
echo "R1: $R1" | |||
echo "R2: $R2" | |||
echo "FAIL" | |||
exit 1 | |||
else | |||
echo "OK" | |||
fi | |||
echo "==> request with unquoted string arg" | |||
R1=$(curl -s 'http://localhost:8008/hello_world?name=abcd&num=123') | |||
R2="{\"jsonrpc\":\"2.0\",\"id\":\"\",\"result\":null,\"error\":\"Error converting http params to args: invalid character 'a' looking for beginning of value\"}" | |||
if [[ "$R1" != "$R2" ]]; then | |||
echo "responses are not identical:" | |||
echo "R1: $R1" | |||
echo "R2: $R2" | |||
echo "FAIL" | |||
exit 1 | |||
else | |||
echo "OK" | |||
fi | |||
echo "==> request with string type when expecting number arg" | |||
R1=$(curl -s 'http://localhost:8008/hello_world?name="abcd"&num=0xabcd') | |||
R2="{\"jsonrpc\":\"2.0\",\"id\":\"\",\"result\":null,\"error\":\"Error converting http params to args: Got a hex string arg, but expected 'int'\"}" | |||
if [[ "$R1" != "$R2" ]]; then | |||
echo "responses are not identical:" | |||
echo "R1: $R1" | |||
echo "R2: $R2" | |||
echo "FAIL" | |||
exit 1 | |||
else | |||
echo "OK" | |||
fi | |||
echo "==> Stopping the server" | |||
kill -9 $PID | |||
rm -f rpcserver | |||
popd | |||
exit 0 |
@ -0,0 +1,35 @@ | |||
package main | |||
import ( | |||
"fmt" | |||
"net/http" | |||
cmn "github.com/tendermint/tmlibs/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 { | |||
cmn.Exit(err.Error()) | |||
} | |||
// Wait forever | |||
cmn.TrapSignal(func() { | |||
}) | |||
} |
@ -0,0 +1,93 @@ | |||
package rpctypes | |||
import ( | |||
"encoding/json" | |||
"strings" | |||
wire "github.com/tendermint/go-wire" | |||
events "github.com/tendermint/tmlibs/events" | |||
) | |||
type RPCRequest struct { | |||
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 { | |||
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 *json.RawMessage `json:"result"` | |||
Error string `json:"error"` | |||
} | |||
func NewRPCResponse(id string, res interface{}, err string) RPCResponse { | |||
var raw *json.RawMessage | |||
if res != nil { | |||
rawMsg := json.RawMessage(wire.JSONBytes(res)) | |||
raw = &rawMsg | |||
} | |||
return RPCResponse{ | |||
JSONRPC: "2.0", | |||
ID: id, | |||
Result: raw, | |||
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 | |||
} | |||
//---------------------------------------- | |||
// 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 | |||
} |
@ -0,0 +1,7 @@ | |||
package rpc | |||
const Maj = "0" | |||
const Min = "7" | |||
const Fix = "0" | |||
const Version = Maj + "." + Min + "." + Fix |