mirror of
https://github.com/tendermint/tendermint.git
synced 2026-07-28 02:52:48 +00:00
Responses are constructed from requests using MakeResponse, MakeError, and MakeErrorf. This ensures the response is always paired with the correct ID, makes cases where there is no ID more explicit at the usage site, and consolidates the handling of error introspection across transports. The logic for unpacking errors and assigning JSON-RPC response types was previously duplicated in three places. Consolidate it in the types package for the RPC subsystem. * update test cases
179 lines
4.9 KiB
Go
179 lines
4.9 KiB
Go
package server
|
|
|
|
import (
|
|
"context"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"reflect"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/tendermint/tendermint/libs/log"
|
|
rpctypes "github.com/tendermint/tendermint/rpc/jsonrpc/types"
|
|
)
|
|
|
|
// uriReqID is a placeholder ID used for GET requests, which do not receive a
|
|
// JSON-RPC request ID from the caller.
|
|
var uriReqID = rpctypes.JSONRPCIntID(-1)
|
|
|
|
// convert from a function name to the http handler
|
|
func makeHTTPHandler(rpcFunc *RPCFunc, logger log.Logger) func(http.ResponseWriter, *http.Request) {
|
|
return func(w http.ResponseWriter, req *http.Request) {
|
|
ctx := rpctypes.WithCallInfo(req.Context(), &rpctypes.CallInfo{
|
|
HTTPRequest: req,
|
|
})
|
|
args, err := parseURLParams(ctx, rpcFunc, req)
|
|
if err != nil {
|
|
w.Header().Set("Content-Type", "text/plain")
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
fmt.Fprintln(w, err.Error())
|
|
return
|
|
}
|
|
jreq := rpctypes.RPCRequest{ID: uriReqID}
|
|
outs := rpcFunc.f.Call(args)
|
|
|
|
logger.Debug("HTTPRestRPC", "method", req.URL.Path, "args", args, "returns", outs)
|
|
result, err := unreflectResult(outs)
|
|
if err == nil {
|
|
writeHTTPResponse(w, logger, jreq.MakeResponse(result))
|
|
} else {
|
|
writeHTTPResponse(w, logger, jreq.MakeError(err))
|
|
}
|
|
}
|
|
}
|
|
|
|
func parseURLParams(ctx context.Context, rf *RPCFunc, req *http.Request) ([]reflect.Value, error) {
|
|
if err := req.ParseForm(); err != nil {
|
|
return nil, fmt.Errorf("invalid HTTP request: %w", err)
|
|
}
|
|
getArg := func(name string) (string, bool) {
|
|
if req.Form.Has(name) {
|
|
return req.Form.Get(name), true
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
vals := make([]reflect.Value, len(rf.argNames)+1)
|
|
vals[0] = reflect.ValueOf(ctx)
|
|
for i, name := range rf.argNames {
|
|
atype := rf.args[i+1]
|
|
|
|
text, ok := getArg(name)
|
|
if !ok {
|
|
vals[i+1] = reflect.Zero(atype)
|
|
continue
|
|
}
|
|
|
|
val, err := parseArgValue(atype, text)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("decoding parameter %q: %w", name, err)
|
|
}
|
|
vals[i+1] = val
|
|
}
|
|
return vals, nil
|
|
}
|
|
|
|
func parseArgValue(atype reflect.Type, text string) (reflect.Value, error) {
|
|
// Regardless whether the argument is a pointer type, allocate a pointer so
|
|
// we can set the computed value.
|
|
var out reflect.Value
|
|
isPtr := atype.Kind() == reflect.Ptr
|
|
if isPtr {
|
|
out = reflect.New(atype.Elem())
|
|
} else {
|
|
out = reflect.New(atype)
|
|
}
|
|
|
|
baseType := out.Type().Elem()
|
|
if isIntType(baseType) {
|
|
// Integral type: Require a base-10 digit string. For compatibility with
|
|
// existing use allow quotation marks.
|
|
v, err := decodeInteger(text)
|
|
if err != nil {
|
|
return reflect.Value{}, fmt.Errorf("invalid integer: %w", err)
|
|
}
|
|
out.Elem().Set(reflect.ValueOf(v).Convert(baseType))
|
|
} else if isStringOrBytes(baseType) {
|
|
// String or byte slice: Check for quotes, hex encoding.
|
|
dec, err := decodeString(text)
|
|
if err != nil {
|
|
return reflect.Value{}, err
|
|
}
|
|
out.Elem().Set(reflect.ValueOf(dec).Convert(baseType))
|
|
|
|
} else if baseType.Kind() == reflect.Bool {
|
|
b, err := strconv.ParseBool(text)
|
|
if err != nil {
|
|
return reflect.Value{}, fmt.Errorf("invalid boolean: %w", err)
|
|
}
|
|
out.Elem().Set(reflect.ValueOf(b))
|
|
|
|
} else {
|
|
// We don't know how to represent other types.
|
|
return reflect.Value{}, fmt.Errorf("unsupported argument type %v", baseType)
|
|
}
|
|
|
|
// If the argument wants a pointer, return the value as-is, otherwise
|
|
// indirect the pointer back off.
|
|
if isPtr {
|
|
return out, nil
|
|
}
|
|
return out.Elem(), nil
|
|
}
|
|
|
|
var uint64Type = reflect.TypeOf(uint64(0))
|
|
|
|
// isIntType reports whether atype is an integer-shaped type.
|
|
func isIntType(atype reflect.Type) bool {
|
|
switch atype.Kind() {
|
|
case reflect.Float32, reflect.Float64:
|
|
return false
|
|
default:
|
|
return atype.ConvertibleTo(uint64Type)
|
|
}
|
|
}
|
|
|
|
// isStringOrBytes reports whether atype is a string or []byte.
|
|
func isStringOrBytes(atype reflect.Type) bool {
|
|
switch atype.Kind() {
|
|
case reflect.String:
|
|
return true
|
|
case reflect.Slice:
|
|
return atype.Elem().Kind() == reflect.Uint8
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// isQuotedString reports whether s is enclosed in double quotes.
|
|
func isQuotedString(s string) bool {
|
|
return len(s) >= 2 && strings.HasPrefix(s, `"`) && strings.HasSuffix(s, `"`)
|
|
}
|
|
|
|
// decodeInteger decodes s into an int64. If s is "double quoted" the quotes
|
|
// are removed; otherwise s must be a base-10 digit string.
|
|
func decodeInteger(s string) (int64, error) {
|
|
if isQuotedString(s) {
|
|
s = s[1 : len(s)-1]
|
|
}
|
|
return strconv.ParseInt(s, 10, 64)
|
|
}
|
|
|
|
// decodeString decodes s into a byte slice. If s has an 0x prefix, it is
|
|
// treated as a hex-encoded string. If it is "double quoted" it is treated as a
|
|
// JSON string value. Otherwise, s is converted to bytes directly.
|
|
func decodeString(s string) ([]byte, error) {
|
|
if lc := strings.ToLower(s); strings.HasPrefix(lc, "0x") {
|
|
return hex.DecodeString(lc[2:])
|
|
} else if isQuotedString(s) {
|
|
var dec string
|
|
if err := json.Unmarshal([]byte(s), &dec); err != nil {
|
|
return nil, fmt.Errorf("invalid quoted string: %w", err)
|
|
}
|
|
return []byte(dec), nil
|
|
}
|
|
return []byte(s), nil
|
|
}
|