Files
tendermint/rpc/jsonrpc/server/http_json_handler.go
M. J. Fromberger 8a684c1bb5 rpc: fix layout of endpoint list (#7742)
* rpc: fix layout of endpoint list

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).

Fixes #3618.
2022-01-31 14:52:38 -08:00

274 lines
7.3 KiB
Go

package server
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"html/template"
"io"
"net/http"
"reflect"
"strconv"
"strings"
"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) {
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,
})
}
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>`))