Files
tendermint/rpc/jsonrpc/server/http_json_handler.go
M. J. Fromberger 32d51cc696 rpc: fix layout of endpoint list (#7742) (#7744)
(backport of #7744)

The output of the default endpoint-list query was not correctly segregating
methods with and without arguments. Fix this, and also clean up the output to
be easier to read (both in code and in generated source).
2022-01-31 15:40:03 -08:00

299 lines
8.4 KiB
Go

package server
import (
"encoding/json"
"errors"
"fmt"
"html/template"
"io/ioutil"
"net/http"
"reflect"
"strings"
tmjson "github.com/tendermint/tendermint/libs/json"
"github.com/tendermint/tendermint/libs/log"
"github.com/tendermint/tendermint/rpc/coretypes"
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, r *http.Request) {
b, err := ioutil.ReadAll(r.Body)
if err != nil {
res := rpctypes.RPCInvalidRequestError(nil,
fmt.Errorf("error reading request body: %w", err),
)
if wErr := WriteRPCResponseHTTPError(w, res); wErr != nil {
logger.Error("failed to write response", "res", res, "err", wErr)
}
return
}
// if its an empty request (like from a browser), just display a list of
// functions
if len(b) == 0 {
writeListOfEndpoints(w, r, funcMap)
return
}
// first try to unmarshal the incoming request as an array of RPC requests
var (
requests []rpctypes.RPCRequest
responses []rpctypes.RPCResponse
)
if err := json.Unmarshal(b, &requests); err != nil {
// next, try to unmarshal as a single request
var request rpctypes.RPCRequest
if err := json.Unmarshal(b, &request); err != nil {
res := rpctypes.RPCParseError(fmt.Errorf("error unmarshaling request: %w", err))
if wErr := WriteRPCResponseHTTPError(w, res); wErr != nil {
logger.Error("failed to write response", "res", res, "err", wErr)
}
return
}
requests = []rpctypes.RPCRequest{request}
}
// Set the default response cache to true unless
// 1. Any RPC request rrror.
// 2. Any RPC request doesn't allow to be cached.
// 3. Any RPC request has the height argument and the value is 0 (the default).
var c = true
for _, request := range requests {
request := request
// A Notification is a Request object without an "id" member.
// The Server MUST NOT reply to a Notification, including those that are within a batch request.
if request.ID == nil {
logger.Debug(
"HTTPJSONRPC received a notification, skipping... (please send a non-empty ID if you want to call a method)",
"req", request,
)
continue
}
if len(r.URL.Path) > 1 {
responses = append(
responses,
rpctypes.RPCInvalidRequestError(request.ID, fmt.Errorf("path %s is invalid", r.URL.Path)),
)
c = false
continue
}
rpcFunc, ok := funcMap[request.Method]
if !ok || rpcFunc.ws {
responses = append(responses, rpctypes.RPCMethodNotFoundError(request.ID))
c = false
continue
}
ctx := &rpctypes.Context{JSONReq: &request, HTTPReq: r}
args := []reflect.Value{reflect.ValueOf(ctx)}
if len(request.Params) > 0 {
fnArgs, err := jsonParamsToArgs(rpcFunc, request.Params)
if err != nil {
responses = append(
responses,
rpctypes.RPCInvalidParamsError(request.ID, fmt.Errorf("error converting json params to arguments: %w", err)),
)
c = false
continue
}
args = append(args, fnArgs...)
}
if hasDefaultHeight(request, args) {
c = false
}
returns := rpcFunc.f.Call(args)
logger.Debug("HTTPJSONRPC", "method", request.Method, "args", args, "returns", returns)
result, err := unreflectResult(returns)
switch e := err.(type) {
// if no error then return a success response
case nil:
responses = append(responses, rpctypes.NewRPCSuccessResponse(request.ID, result))
// if this already of type RPC error then forward that error
case *rpctypes.RPCError:
responses = append(responses, rpctypes.NewRPCErrorResponse(request.ID, e.Code, e.Message, e.Data))
c = false
default: // we need to unwrap the error and parse it accordingly
switch errors.Unwrap(err) {
// check if the error was due to an invald request
case coretypes.ErrZeroOrNegativeHeight, coretypes.ErrZeroOrNegativePerPage,
coretypes.ErrPageOutOfRange, coretypes.ErrInvalidRequest:
responses = append(responses, rpctypes.RPCInvalidRequestError(request.ID, err))
c = false
// lastly default all remaining errors as internal errors
default: // includes ctypes.ErrHeightNotAvailable and ctypes.ErrHeightExceedsChainHead
responses = append(responses, rpctypes.RPCInternalError(request.ID, err))
c = false
}
}
if c && !rpcFunc.cache {
c = false
}
}
if len(responses) > 0 {
if wErr := WriteRPCResponseHTTP(w, c, responses...); wErr != nil {
logger.Error("failed to write responses", "err", wErr)
}
}
}
}
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)
}
}
func mapParamsToArgs(
rpcFunc *RPCFunc,
params map[string]json.RawMessage,
argsOffset int,
) ([]reflect.Value, error) {
values := make([]reflect.Value, len(rpcFunc.argNames))
for i, argName := range rpcFunc.argNames {
argType := rpcFunc.args[i+argsOffset]
if p, ok := params[argName]; ok && p != nil && len(p) > 0 {
val := reflect.New(argType)
err := tmjson.Unmarshal(p, val.Interface())
if err != nil {
return nil, err
}
values[i] = val.Elem()
} else { // use default for that type
values[i] = reflect.Zero(argType)
}
}
return values, nil
}
func arrayParamsToArgs(
rpcFunc *RPCFunc,
params []json.RawMessage,
argsOffset int,
) ([]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(params))
for i, p := range params {
argType := rpcFunc.args[i+argsOffset]
val := reflect.New(argType)
err := tmjson.Unmarshal(p, val.Interface())
if err != nil {
return nil, err
}
values[i] = val.Elem()
}
return values, nil
}
// raw is unparsed json (from json.RawMessage) encoding either a map or an
// array.
//
// Example:
// rpcFunc.args = [rpctypes.Context string]
// rpcFunc.argNames = ["arg"]
func jsonParamsToArgs(rpcFunc *RPCFunc, raw []byte) ([]reflect.Value, error) {
const argsOffset = 1
// TODO: Make more efficient, perhaps by checking the first character for '{' or '['?
// First, try to get the map.
var m map[string]json.RawMessage
err := json.Unmarshal(raw, &m)
if err == nil {
return mapParamsToArgs(rpcFunc, m, argsOffset)
}
// Otherwise, try an array.
var a []json.RawMessage
err = json.Unmarshal(raw, &a)
if err == nil {
return arrayParamsToArgs(rpcFunc, a, argsOffset)
}
// Otherwise, bad format, we cannot parse
return nil, fmt.Errorf("unknown type for JSON params: %v. Expected map or array", err)
}
// writes a list of available rpc endpoints as an html page
func writeListOfEndpoints(w http.ResponseWriter, r *http.Request, funcMap map[string]*RPCFunc) {
hasArgs := make(map[string]string)
noArgs := make(map[string]string)
for name, rf := range funcMap {
base := fmt.Sprintf("//%s/%s", r.Host, name)
// N.B. Check argNames, not args, since the type list includes the type
// of the leading context argument.
if len(rf.argNames) == 0 {
noArgs[name] = base
} else {
query := append([]string(nil), rf.argNames...)
for i, arg := range query {
query[i] = arg + "=_"
}
hasArgs[name] = base + "?" + strings.Join(query, "&")
}
}
w.Header().Set("Content-Type", "text/html")
_ = listOfEndpoints.Execute(w, map[string]map[string]string{
"NoArgs": noArgs,
"HasArgs": hasArgs,
})
}
func hasDefaultHeight(r rpctypes.RPCRequest, h []reflect.Value) bool {
switch r.Method {
case "block", "block_results", "commit", "consensus_params", "validators":
return len(h) < 2 || h[1].IsZero()
default:
return false
}
}
var listOfEndpoints = template.Must(template.New("list").Parse(`<html>
<head><title>List of RPC Endpoints</title></head>
<body>
<h1>Available RPC endpoints:</h1>
{{if .NoArgs}}
<hr />
<h2>Endpoints with no arguments:</h2>
<ul>
{{range $link := .NoArgs}} <li><a href="{{$link}}">{{$link}}</a></li>
{{end -}}
</ul>{{end}}
{{if .HasArgs}}
<hr />
<h2>Endpoints that require arguments:</h2>
<ul>
{{range $link := .HasArgs}} <li><a href="{{$link}}">{{$link}}</a></li>
{{end -}}
</ul>{{end}}
</body></html>`))