// Package rest provides HTTP types and primitives for REST // requests validation and responses handling. package rest import ( "encoding/json" "errors" "fmt" "io/ioutil" "net/http" "net/url" "strconv" "strings" "github.com/tendermint/tendermint/types" "github.com/cosmos/cosmos-sdk/client/context" "github.com/cosmos/cosmos-sdk/codec" sdk "github.com/cosmos/cosmos-sdk/types" ) const ( DefaultPage = 1 DefaultLimit = 30 // should be consistent with tendermint/tendermint/rpc/core/pipe.go:19 TxMinHeightKey = "tx.minheight" // Inclusive minimum height filter TxMaxHeightKey = "tx.maxheight" // Inclusive maximum height filter ) // ResponseWithHeight defines a response object type that wraps an original // response with a height. type ResponseWithHeight struct { Height int64 `json:"height"` Result json.RawMessage `json:"result"` } // NewResponseWithHeight creates a new ResponseWithHeight instance func NewResponseWithHeight(height int64, result json.RawMessage) ResponseWithHeight { return ResponseWithHeight{ Height: height, Result: result, } } // GasEstimateResponse defines a response definition for tx gas estimation. type GasEstimateResponse struct { GasEstimate uint64 `json:"gas_estimate"` } // BaseReq defines a structure that can be embedded in other request structures // that all share common "base" fields. type BaseReq struct { From string `json:"from"` Memo string `json:"memo"` ChainID string `json:"chain_id"` AccountNumber uint64 `json:"account_number"` Sequence uint64 `json:"sequence"` Fees sdk.Coins `json:"fees"` GasPrices sdk.DecCoins `json:"gas_prices"` Gas string `json:"gas"` GasAdjustment string `json:"gas_adjustment"` Simulate bool `json:"simulate"` } // NewBaseReq creates a new basic request instance and sanitizes its values func NewBaseReq( from, memo, chainID string, gas, gasAdjustment string, accNumber, seq uint64, fees sdk.Coins, gasPrices sdk.DecCoins, simulate bool, ) BaseReq { return BaseReq{ From: strings.TrimSpace(from), Memo: strings.TrimSpace(memo), ChainID: strings.TrimSpace(chainID), Fees: fees, GasPrices: gasPrices, Gas: strings.TrimSpace(gas), GasAdjustment: strings.TrimSpace(gasAdjustment), AccountNumber: accNumber, Sequence: seq, Simulate: simulate, } } // Sanitize performs basic sanitization on a BaseReq object. func (br BaseReq) Sanitize() BaseReq { return NewBaseReq( br.From, br.Memo, br.ChainID, br.Gas, br.GasAdjustment, br.AccountNumber, br.Sequence, br.Fees, br.GasPrices, br.Simulate, ) } // ValidateBasic performs basic validation of a BaseReq. If custom validation // logic is needed, the implementing request handler should perform those // checks manually. func (br BaseReq) ValidateBasic(w http.ResponseWriter) bool { if !br.Simulate { switch { case len(br.ChainID) == 0: WriteErrorResponse(w, http.StatusUnauthorized, "chain-id required but not specified") return false case !br.Fees.IsZero() && !br.GasPrices.IsZero(): // both fees and gas prices were provided WriteErrorResponse(w, http.StatusBadRequest, "cannot provide both fees and gas prices") return false case !br.Fees.IsValid() && !br.GasPrices.IsValid(): // neither fees or gas prices were provided WriteErrorResponse(w, http.StatusPaymentRequired, "invalid fees or gas prices provided") return false } } if _, err := sdk.AccAddressFromBech32(br.From); err != nil || len(br.From) == 0 { WriteErrorResponse(w, http.StatusUnauthorized, fmt.Sprintf("invalid from address: %s", br.From)) return false } return true } // ReadRESTReq reads and unmarshals a Request's body to the the BaseReq stuct. // Writes an error response to ResponseWriter and returns true if errors occurred. func ReadRESTReq(w http.ResponseWriter, r *http.Request, cdc *codec.Codec, req interface{}) bool { body, err := ioutil.ReadAll(r.Body) if err != nil { WriteErrorResponse(w, http.StatusBadRequest, err.Error()) return false } err = cdc.UnmarshalJSON(body, req) if err != nil { WriteErrorResponse(w, http.StatusBadRequest, fmt.Sprintf("failed to decode JSON payload: %s", err)) return false } return true } // ErrorResponse defines the attributes of a JSON error response. type ErrorResponse struct { Code int `json:"code,omitempty"` Error string `json:"error"` } // NewErrorResponse creates a new ErrorResponse instance. func NewErrorResponse(code int, err string) ErrorResponse { return ErrorResponse{Code: code, Error: err} } // WriteErrorResponse prepares and writes a HTTP error // given a status code and an error message. func WriteErrorResponse(w http.ResponseWriter, status int, err string) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) _, _ = w.Write(codec.Cdc.MustMarshalJSON(NewErrorResponse(0, err))) } // WriteSimulationResponse prepares and writes an HTTP // response for transactions simulations. func WriteSimulationResponse(w http.ResponseWriter, cdc *codec.Codec, gas uint64) { gasEst := GasEstimateResponse{GasEstimate: gas} resp, err := cdc.MarshalJSON(gasEst) if err != nil { WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) return } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) _, _ = w.Write(resp) } // ParseInt64OrReturnBadRequest converts s to a int64 value. func ParseInt64OrReturnBadRequest(w http.ResponseWriter, s string) (n int64, ok bool) { var err error n, err = strconv.ParseInt(s, 10, 64) if err != nil { err := fmt.Errorf("'%s' is not a valid int64", s) WriteErrorResponse(w, http.StatusBadRequest, err.Error()) return n, false } return n, true } // ParseUint64OrReturnBadRequest converts s to a uint64 value. func ParseUint64OrReturnBadRequest(w http.ResponseWriter, s string) (n uint64, ok bool) { var err error n, err = strconv.ParseUint(s, 10, 64) if err != nil { err := fmt.Errorf("'%s' is not a valid uint64", s) WriteErrorResponse(w, http.StatusBadRequest, err.Error()) return n, false } return n, true } // ParseFloat64OrReturnBadRequest converts s to a float64 value. It returns a // default value, defaultIfEmpty, if the string is empty. func ParseFloat64OrReturnBadRequest(w http.ResponseWriter, s string, defaultIfEmpty float64) (n float64, ok bool) { if len(s) == 0 { return defaultIfEmpty, true } n, err := strconv.ParseFloat(s, 64) if err != nil { WriteErrorResponse(w, http.StatusBadRequest, err.Error()) return n, false } return n, true } // ParseQueryHeightOrReturnBadRequest sets the height to execute a query if set by the http request. // It returns false if there was an error parsing the height. func ParseQueryHeightOrReturnBadRequest(w http.ResponseWriter, cliCtx context.CLIContext, r *http.Request) (context.CLIContext, bool) { heightStr := r.FormValue("height") if heightStr != "" { height, err := strconv.ParseInt(heightStr, 10, 64) if err != nil { WriteErrorResponse(w, http.StatusBadRequest, err.Error()) return cliCtx, false } if height < 0 { WriteErrorResponse(w, http.StatusBadRequest, "height must be equal or greater than zero") return cliCtx, false } if height > 0 { cliCtx = cliCtx.WithHeight(height) } } else { cliCtx = cliCtx.WithHeight(0) } return cliCtx, true } // PostProcessResponseBare post processes a body similar to PostProcessResponse // except it does not wrap the body and inject the height. func PostProcessResponseBare(w http.ResponseWriter, cliCtx context.CLIContext, body interface{}) { var ( resp []byte err error ) switch b := body.(type) { case []byte: resp = b default: if cliCtx.Indent { resp, err = cliCtx.Codec.MarshalJSONIndent(body, "", " ") } else { resp, err = cliCtx.Codec.MarshalJSON(body) } if err != nil { WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) return } } w.Header().Set("Content-Type", "application/json") _, _ = w.Write(resp) } // PostProcessResponse performs post processing for a REST response. The result // returned to clients will contain two fields, the height at which the resource // was queried at and the original result. func PostProcessResponse(w http.ResponseWriter, cliCtx context.CLIContext, resp interface{}) { var result []byte if cliCtx.Height < 0 { WriteErrorResponse(w, http.StatusInternalServerError, fmt.Errorf("negative height in response").Error()) return } switch res := resp.(type) { case []byte: result = res default: var err error if cliCtx.Indent { result, err = cliCtx.Codec.MarshalJSONIndent(resp, "", " ") } else { result, err = cliCtx.Codec.MarshalJSON(resp) } if err != nil { WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) return } } wrappedResp := NewResponseWithHeight(cliCtx.Height, result) var ( output []byte err error ) if cliCtx.Indent { output, err = cliCtx.Codec.MarshalJSONIndent(wrappedResp, "", " ") } else { output, err = cliCtx.Codec.MarshalJSON(wrappedResp) } if err != nil { WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) return } w.Header().Set("Content-Type", "application/json") _, _ = w.Write(output) } // ParseHTTPArgsWithLimit parses the request's URL and returns a slice containing // all arguments pairs. It separates page and limit used for pagination where a // default limit can be provided. func ParseHTTPArgsWithLimit(r *http.Request, defaultLimit int) (tags []string, page, limit int, err error) { tags = make([]string, 0, len(r.Form)) for key, values := range r.Form { if key == "page" || key == "limit" { continue } var value string value, err = url.QueryUnescape(values[0]) if err != nil { return tags, page, limit, err } var tag string switch key { case types.TxHeightKey: tag = fmt.Sprintf("%s=%s", key, value) case TxMinHeightKey: tag = fmt.Sprintf("%s>=%s", types.TxHeightKey, value) case TxMaxHeightKey: tag = fmt.Sprintf("%s<=%s", types.TxHeightKey, value) default: tag = fmt.Sprintf("%s='%s'", key, value) } tags = append(tags, tag) } pageStr := r.FormValue("page") if pageStr == "" { page = DefaultPage } else { page, err = strconv.Atoi(pageStr) if err != nil { return tags, page, limit, err } else if page <= 0 { return tags, page, limit, errors.New("page must greater than 0") } } limitStr := r.FormValue("limit") if limitStr == "" { limit = defaultLimit } else { limit, err = strconv.Atoi(limitStr) if err != nil { return tags, page, limit, err } else if limit <= 0 { return tags, page, limit, errors.New("limit must greater than 0") } } return tags, page, limit, nil } // ParseHTTPArgs parses the request's URL and returns a slice containing all // arguments pairs. It separates page and limit used for pagination. func ParseHTTPArgs(r *http.Request) (tags []string, page, limit int, err error) { return ParseHTTPArgsWithLimit(r, DefaultLimit) } // ParseQueryParamBool parses the given param to a boolean. It returns false by // default if the string is not parseable to bool. func ParseQueryParamBool(r *http.Request, param string) bool { valueStr := r.FormValue(param) value := false if ok, err := strconv.ParseBool(valueStr); err == nil { value = ok } return value }