package server import ( "bytes" "encoding/json" "errors" "fmt" "io" "net/http" "reflect" "sort" 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 := io.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) { noArgNames := []string{} argNames := []string{} for name, funcData := range funcMap { if len(funcData.args) == 0 { noArgNames = append(noArgNames, name) } else { argNames = append(argNames, name) } } sort.Strings(noArgNames) sort.Strings(argNames) buf := new(bytes.Buffer) buf.WriteString("") buf.WriteString("
Available endpoints:
") for _, name := range noArgNames { link := fmt.Sprintf("//%s/%s", r.Host, name) buf.WriteString(fmt.Sprintf("%s
", link, link)) } buf.WriteString("
Endpoints that require arguments:
") for _, name := range argNames { link := fmt.Sprintf("//%s/%s?", r.Host, name) funcData := funcMap[name] for i, argName := range funcData.argNames { link += argName + "=_" if i < len(funcData.argNames)-1 { link += "&" } } buf.WriteString(fmt.Sprintf("%s
", link, link)) } buf.WriteString("") w.Header().Set("Content-Type", "text/html") w.WriteHeader(200) w.Write(buf.Bytes()) // nolint: errcheck } 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 } }