Files
tendermint/rpc/jsonrpc/server/http_uri_handler.go
mergify[bot] 7917485bc7 rpc: Add caching support (backport #9650) (#9666)
* rpc: Add caching support (#9650)

* Set cache control in the HTTP-RPC response header

* Add a simply cache policy to the RPC routes

* add a condition to check the RPC request has default height settings

* fix cherry pick error

* update pending log

* use options struct intead of single parameter

* refacor FuncOptions to functional options

* add functional options in WebSocket RPC function

* revert doc

* replace deprecated function call

* revise functional options

* remove unuse comment

* fix revised error

* adjust cache-control settings

* Update rpc/jsonrpc/server/http_json_handler.go

Co-authored-by: Thane Thomson <connect@thanethomson.com>

* linter: Fix false positive

Signed-off-by: Thane Thomson <connect@thanethomson.com>

* rpc: Separate cacheable and non-cacheable HTTP response writers

Allows us to roll this change out in a non-API-breaking way, since this
is an additive change.

Signed-off-by: Thane Thomson <connect@thanethomson.com>

* rpc: Ensure consistent caching strategy

Ensure a consistent caching strategy across both JSONRPC- and URI-based
requests.

This requires a bit of a refactor of the previous caching logic, which
is complicated a little by the complex reflection-based approach taken
in the Tendermint RPC.

Signed-off-by: Thane Thomson <connect@thanethomson.com>

* rpc: Add more tests for caching

Signed-off-by: Thane Thomson <connect@thanethomson.com>

* Update CHANGELOG_PENDING

Signed-off-by: Thane Thomson <connect@thanethomson.com>

* light: Sync routes config with RPC core

Signed-off-by: Thane Thomson <connect@thanethomson.com>

* rpc: Update OpenAPI docs

Signed-off-by: Thane Thomson <connect@thanethomson.com>

Signed-off-by: Thane Thomson <connect@thanethomson.com>
Co-authored-by: jayt106 <jaytseng106@gmail.com>
Co-authored-by: jay tseng <jay.tseng@crypto.com>
Co-authored-by: JayT106 <JayT106@users.noreply.github.com>
(cherry picked from commit 816c6bac00)

# Conflicts:
#	CHANGELOG_PENDING.md
#	light/proxy/routes.go
#	rpc/core/routes.go
#	rpc/openapi/openapi.yaml
#	test/fuzz/tests/rpc_jsonrpc_server_test.go

* Fix conflict in CHANGELOG_PENDING

Signed-off-by: Thane Thomson <connect@thanethomson.com>

* Resolve remaining conflicts

Signed-off-by: Thane Thomson <connect@thanethomson.com>

Signed-off-by: Thane Thomson <connect@thanethomson.com>
Co-authored-by: Thane Thomson <connect@thanethomson.com>
2022-11-07 06:11:05 -05:00

219 lines
5.4 KiB
Go

package server
import (
"encoding/hex"
"fmt"
"net/http"
"reflect"
"regexp"
"strings"
tmjson "github.com/tendermint/tendermint/libs/json"
"github.com/tendermint/tendermint/libs/log"
types "github.com/tendermint/tendermint/rpc/jsonrpc/types"
)
// HTTP + URI handler
var reInt = regexp.MustCompile(`^-?[0-9]+$`)
// convert from a function name to the http handler
func makeHTTPHandler(rpcFunc *RPCFunc, logger log.Logger) func(http.ResponseWriter, *http.Request) {
// Always return -1 as there's no ID here.
dummyID := types.JSONRPCIntID(-1) // URIClientRequestID
// Exception for websocket endpoints
if rpcFunc.ws {
return func(w http.ResponseWriter, r *http.Request) {
res := types.RPCMethodNotFoundError(dummyID)
if wErr := WriteRPCResponseHTTPError(w, http.StatusNotFound, res); wErr != nil {
logger.Error("failed to write response", "res", res, "err", wErr)
}
}
}
// All other endpoints
return func(w http.ResponseWriter, r *http.Request) {
logger.Debug("HTTP HANDLER", "req", r)
ctx := &types.Context{HTTPReq: r}
args := []reflect.Value{reflect.ValueOf(ctx)}
fnArgs, err := httpParamsToArgs(rpcFunc, r)
if err != nil {
res := types.RPCInvalidParamsError(dummyID,
fmt.Errorf("error converting http params to arguments: %w", err),
)
if wErr := WriteRPCResponseHTTPError(w, http.StatusInternalServerError, res); wErr != nil {
logger.Error("failed to write response", "res", res, "err", wErr)
}
return
}
args = append(args, fnArgs...)
returns := rpcFunc.f.Call(args)
logger.Debug("HTTPRestRPC", "method", r.URL.Path, "args", args, "returns", returns)
result, err := unreflectResult(returns)
if err != nil {
if err := WriteRPCResponseHTTPError(w, http.StatusInternalServerError,
types.RPCInternalError(dummyID, err)); err != nil {
logger.Error("failed to write response", "res", result, "err", err)
return
}
return
}
resp := types.NewRPCSuccessResponse(dummyID, result)
if rpcFunc.cacheableWithArgs(args) {
err = WriteCacheableRPCResponseHTTP(w, resp)
} else {
err = WriteRPCResponseHTTP(w, resp)
}
if err != nil {
logger.Error("failed to write response", "res", result, "err", err)
return
}
}
}
// Covert an http query to a list of properly typed values.
// To be properly decoded the arg must be a concrete type from tendermint (if its an interface).
func httpParamsToArgs(rpcFunc *RPCFunc, r *http.Request) ([]reflect.Value, error) {
// skip types.Context
const argsOffset = 1
values := make([]reflect.Value, len(rpcFunc.argNames))
for i, name := range rpcFunc.argNames {
argType := rpcFunc.args[i+argsOffset]
values[i] = reflect.Zero(argType) // set default for that type
arg := getParam(r, name)
// log.Notice("param to arg", "argType", argType, "name", name, "arg", arg)
if arg == "" {
continue
}
v, ok, err := nonJSONStringToArg(argType, arg)
if err != nil {
return nil, err
}
if ok {
values[i] = v
continue
}
values[i], err = jsonStringToArg(argType, arg)
if err != nil {
return nil, err
}
}
return values, nil
}
func jsonStringToArg(rt reflect.Type, arg string) (reflect.Value, error) {
rv := reflect.New(rt)
err := tmjson.Unmarshal([]byte(arg), rv.Interface())
if err != nil {
return rv, err
}
rv = rv.Elem()
return rv, nil
}
func nonJSONStringToArg(rt reflect.Type, arg string) (reflect.Value, bool, error) {
if rt.Kind() == reflect.Ptr {
rv1, ok, err := nonJSONStringToArg(rt.Elem(), arg)
switch {
case err != nil:
return reflect.Value{}, false, err
case ok:
rv := reflect.New(rt.Elem())
rv.Elem().Set(rv1)
return rv, true, nil
default:
return reflect.Value{}, false, nil
}
} else {
return _nonJSONStringToArg(rt, arg)
}
}
// NOTE: rt.Kind() isn't a pointer.
func _nonJSONStringToArg(rt reflect.Type, arg string) (reflect.Value, bool, error) {
isIntString := reInt.Match([]byte(arg))
isQuotedString := strings.HasPrefix(arg, `"`) && strings.HasSuffix(arg, `"`)
isHexString := strings.HasPrefix(strings.ToLower(arg), "0x")
var expectingString, expectingByteSlice, expectingInt bool
switch rt.Kind() {
case reflect.Int,
reflect.Uint,
reflect.Int8,
reflect.Uint8,
reflect.Int16,
reflect.Uint16,
reflect.Int32,
reflect.Uint32,
reflect.Int64,
reflect.Uint64:
expectingInt = true
case reflect.String:
expectingString = true
case reflect.Slice:
expectingByteSlice = rt.Elem().Kind() == reflect.Uint8
}
if isIntString && expectingInt {
qarg := `"` + arg + `"`
rv, err := jsonStringToArg(rt, qarg)
if err != nil {
return rv, false, err
}
return rv, true, nil
}
if isHexString {
if !expectingString && !expectingByteSlice {
err := fmt.Errorf("got a hex string arg, but expected '%s'",
rt.Kind().String())
return reflect.ValueOf(nil), false, err
}
var value []byte
value, err := hex.DecodeString(arg[2:])
if err != nil {
return reflect.ValueOf(nil), false, err
}
if rt.Kind() == reflect.String {
return reflect.ValueOf(string(value)), true, nil
}
return reflect.ValueOf(value), true, nil
}
if isQuotedString && expectingByteSlice {
v := reflect.New(reflect.TypeOf(""))
err := tmjson.Unmarshal([]byte(arg), v.Interface())
if err != nil {
return reflect.ValueOf(nil), false, err
}
v = v.Elem()
return reflect.ValueOf([]byte(v.String())), true, nil
}
return reflect.ValueOf(nil), false, nil
}
func getParam(r *http.Request, param string) string {
s := r.URL.Query().Get(param)
if s == "" {
s = r.FormValue(param)
}
return s
}