mirror of
https://github.com/tendermint/tendermint.git
synced 2026-02-06 03:50:46 +00:00
(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).
299 lines
8.4 KiB
Go
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>`))
|