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