package server import ( "context" "encoding/hex" "encoding/json" "errors" "fmt" "net/http" "reflect" "strconv" "strings" "github.com/tendermint/tendermint/libs/log" "github.com/tendermint/tendermint/rpc/coretypes" rpctypes "github.com/tendermint/tendermint/rpc/jsonrpc/types" ) // HTTP + URI handler // 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 := rpctypes.JSONRPCIntID(-1) // URIClientRequestID // Exception for websocket endpoints // // TODO(creachadair): Rather than reporting errors for these, we should // remove them from the routing list entirely on this endpoint. if rpcFunc.ws { return func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNotFound) } } // All other endpoints 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 } outs := rpcFunc.f.Call(args) logger.Debug("HTTPRestRPC", "method", req.URL.Path, "args", args, "returns", outs) result, err := unreflectResult(outs) switch e := err.(type) { // if no error then return a success response case nil: writeHTTPResponse(w, logger, rpctypes.NewRPCSuccessResponse(dummyID, result)) // if this already of type RPC error then forward that error. case *rpctypes.RPCError: writeHTTPResponse(w, logger, rpctypes.NewRPCErrorResponse(dummyID, e.Code, e.Message, e.Data)) default: // we need to unwrap the error and parse it accordingly switch errors.Unwrap(err) { case coretypes.ErrZeroOrNegativeHeight, coretypes.ErrZeroOrNegativePerPage, coretypes.ErrPageOutOfRange, coretypes.ErrInvalidRequest: writeHTTPResponse(w, logger, rpctypes.RPCInvalidRequestError(dummyID, err)) default: // ctypes.ErrHeightNotAvailable, ctypes.ErrHeightExceedsChainHead: writeHTTPResponse(w, logger, rpctypes.RPCInternalError(dummyID, 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 }