Files
tendermint/rpc/jsonrpc/server/http_json_handler.go
M. J. Fromberger fbe86ea645 rpc: simplify and consolidate response construction (#7725)
Responses are constructed from requests using MakeResponse, MakeError, and
MakeErrorf. This ensures the response is always paired with the correct ID,
makes cases where there is no ID more explicit at the usage site, and
consolidates the handling of error introspection across transports.

The logic for unpacking errors and assigning JSON-RPC response types was
previously duplicated in three places. Consolidate it in the types package for
the RPC subsystem.

* update test cases
2022-01-28 13:33:02 -08:00

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.ID == nil {
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
}