mirror of
https://github.com/tendermint/tendermint.git
synced 2026-02-03 18:42:14 +00:00
(cherry picked from commit 931c98f7ad)
Co-authored-by: Sam Kleinman <garen@tychoish.com>
Co-authored-by: M. J. Fromberger <fromberger@interchain.io>
305 lines
8.5 KiB
Go
305 lines
8.5 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 ensureBodyClose(next http.HandlerFunc) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
defer r.Body.Close()
|
|
next(w, r)
|
|
}
|
|
}
|
|
|
|
func handleInvalidJSONRPCPaths(next http.HandlerFunc) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
// 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>`))
|