mirror of
https://github.com/tendermint/tendermint.git
synced 2026-09-02 06:07:08 +00:00
* rpc: simplify the handling of JSON-RPC request and response IDs Replace the ID wrapper interface with plain JSON. Internally, the client libraries use only integer IDs, and the server does not care about the ID structure apart from checking its validity. Basic structure of this change: - Remove the jsonrpcid interface and its helpers. - Unexport the ID field of request and response. - Add helpers for constructing requests and responses. - Fix up usage and tests.
262 lines
7.2 KiB
Go
262 lines
7.2 KiB
Go
package server
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"reflect"
|
|
"sort"
|
|
"strconv"
|
|
|
|
"github.com/tendermint/tendermint/libs/log"
|
|
rpctypes "github.com/tendermint/tendermint/rpc/jsonrpc/types"
|
|
)
|
|
|
|
// HTTP + JSON handler
|
|
|
|
// jsonrpc calls grab the given method's function info and runs reflect.Call
|
|
func makeJSONRPCHandler(funcMap map[string]*RPCFunc, logger log.Logger) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, hreq *http.Request) {
|
|
// For POST requests, reject a non-root URL path. This should not happen
|
|
// in the standard configuration, since the wrapper checks the path.
|
|
if hreq.URL.Path != "/" {
|
|
writeRPCResponse(w, logger, rpctypes.RPCRequest{}.MakeErrorf(
|
|
rpctypes.CodeInvalidRequest, "invalid path: %q", hreq.URL.Path))
|
|
return
|
|
}
|
|
|
|
b, err := io.ReadAll(hreq.Body)
|
|
if err != nil {
|
|
writeRPCResponse(w, logger, rpctypes.RPCRequest{}.MakeErrorf(
|
|
rpctypes.CodeInvalidRequest, "reading request body: %v", err))
|
|
return
|
|
}
|
|
|
|
// if its an empty request (like from a browser), just display a list of
|
|
// functions
|
|
if len(b) == 0 {
|
|
writeListOfEndpoints(w, hreq, funcMap)
|
|
return
|
|
}
|
|
|
|
requests, err := parseRequests(b)
|
|
if err != nil {
|
|
writeRPCResponse(w, logger, rpctypes.RPCRequest{}.MakeErrorf(
|
|
rpctypes.CodeParseError, "decoding request: %v", err))
|
|
return
|
|
}
|
|
|
|
var responses []rpctypes.RPCResponse
|
|
for _, req := range requests {
|
|
// Ignore notifications, which this service does not support.
|
|
if req.IsNotification() {
|
|
logger.Debug("Ignoring notification", "req", req)
|
|
continue
|
|
}
|
|
|
|
rpcFunc, ok := funcMap[req.Method]
|
|
if !ok || rpcFunc.ws {
|
|
responses = append(responses, req.MakeErrorf(rpctypes.CodeMethodNotFound, req.Method))
|
|
continue
|
|
}
|
|
|
|
req := req
|
|
ctx := rpctypes.WithCallInfo(hreq.Context(), &rpctypes.CallInfo{
|
|
RPCRequest: &req,
|
|
HTTPRequest: hreq,
|
|
})
|
|
args, err := parseParams(ctx, rpcFunc, req.Params)
|
|
if err != nil {
|
|
responses = append(responses,
|
|
req.MakeErrorf(rpctypes.CodeInvalidParams, "converting JSON parameters: %v", err))
|
|
continue
|
|
}
|
|
|
|
returns := rpcFunc.f.Call(args)
|
|
result, err := unreflectResult(returns)
|
|
if err == nil {
|
|
responses = append(responses, req.MakeResponse(result))
|
|
} else {
|
|
responses = append(responses, req.MakeError(err))
|
|
}
|
|
}
|
|
|
|
if len(responses) == 0 {
|
|
return
|
|
}
|
|
writeRPCResponse(w, logger, responses...)
|
|
}
|
|
}
|
|
|
|
func handleInvalidJSONRPCPaths(next http.HandlerFunc) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
// Since the pattern "/" matches all paths not matched by other registered patterns,
|
|
// we check whether the path is indeed "/", otherwise return a 404 error
|
|
if r.URL.Path != "/" {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
|
|
next(w, r)
|
|
}
|
|
}
|
|
|
|
// parseRequests parses a JSON-RPC request or request batch from data.
|
|
func parseRequests(data []byte) ([]rpctypes.RPCRequest, error) {
|
|
var reqs []rpctypes.RPCRequest
|
|
var err error
|
|
|
|
isArray := bytes.HasPrefix(bytes.TrimSpace(data), []byte("["))
|
|
if isArray {
|
|
err = json.Unmarshal(data, &reqs)
|
|
} else {
|
|
reqs = append(reqs, rpctypes.RPCRequest{})
|
|
err = json.Unmarshal(data, &reqs[0])
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return reqs, nil
|
|
}
|
|
|
|
// parseParams parses the JSON parameters of rpcReq into the arguments of fn,
|
|
// returning the corresponding argument values or an error.
|
|
func parseParams(ctx context.Context, fn *RPCFunc, paramData []byte) ([]reflect.Value, error) {
|
|
params, err := parseJSONParams(fn, paramData)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
args := make([]reflect.Value, 1+len(params))
|
|
args[0] = reflect.ValueOf(ctx)
|
|
for i, param := range params {
|
|
ptype := fn.args[i+1]
|
|
if len(param) == 0 {
|
|
args[i+1] = reflect.Zero(ptype)
|
|
continue
|
|
}
|
|
|
|
var pval reflect.Value
|
|
isPtr := ptype.Kind() == reflect.Ptr
|
|
if isPtr {
|
|
pval = reflect.New(ptype.Elem())
|
|
} else {
|
|
pval = reflect.New(ptype)
|
|
}
|
|
baseType := pval.Type().Elem()
|
|
|
|
if isIntType(baseType) && isStringValue(param) {
|
|
var z int64String
|
|
if err := json.Unmarshal(param, &z); err != nil {
|
|
return nil, fmt.Errorf("decoding string %q: %w", fn.argNames[i], err)
|
|
}
|
|
pval.Elem().Set(reflect.ValueOf(z).Convert(baseType))
|
|
} else if err := json.Unmarshal(param, pval.Interface()); err != nil {
|
|
return nil, fmt.Errorf("decoding %q: %w", fn.argNames[i], err)
|
|
}
|
|
|
|
if isPtr {
|
|
args[i+1] = pval
|
|
} else {
|
|
args[i+1] = pval.Elem()
|
|
}
|
|
}
|
|
return args, nil
|
|
}
|
|
|
|
// parseJSONParams parses data and returns a slice of JSON values matching the
|
|
// positional parameters of fn. It reports an error if data is not "null" and
|
|
// does not encode an object or an array, or if the number of array parameters
|
|
// does not match the argument list of fn (excluding the context).
|
|
func parseJSONParams(fn *RPCFunc, data []byte) ([]json.RawMessage, error) {
|
|
base := bytes.TrimSpace(data)
|
|
if bytes.HasPrefix(base, []byte("{")) {
|
|
var m map[string]json.RawMessage
|
|
if err := json.Unmarshal(base, &m); err != nil {
|
|
return nil, fmt.Errorf("decoding parameter object: %w", err)
|
|
}
|
|
out := make([]json.RawMessage, len(fn.argNames))
|
|
for i, name := range fn.argNames {
|
|
if p, ok := m[name]; ok {
|
|
out[i] = p
|
|
}
|
|
}
|
|
return out, nil
|
|
|
|
} else if bytes.HasPrefix(base, []byte("[")) {
|
|
var m []json.RawMessage
|
|
if err := json.Unmarshal(base, &m); err != nil {
|
|
return nil, fmt.Errorf("decoding parameter array: %w", err)
|
|
}
|
|
if len(m) != len(fn.argNames) {
|
|
return nil, fmt.Errorf("got %d parameters, want %d", len(m), len(fn.argNames))
|
|
}
|
|
return m, nil
|
|
|
|
} else if bytes.Equal(base, []byte("null")) {
|
|
return make([]json.RawMessage, len(fn.argNames)), nil
|
|
}
|
|
|
|
return nil, errors.New("parameters must be an object or an array")
|
|
}
|
|
|
|
// isStringValue reports whether data is a JSON string value.
|
|
func isStringValue(data json.RawMessage) bool {
|
|
return len(data) != 0 && data[0] == '"'
|
|
}
|
|
|
|
type int64String int64
|
|
|
|
func (z *int64String) UnmarshalText(data []byte) error {
|
|
v, err := strconv.ParseInt(string(data), 10, 64)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
*z = int64String(v)
|
|
return 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("//%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("//%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()) // nolint: errcheck
|
|
}
|