diff --git a/api/api.go b/api/api.go index 7084950b..32bb5c27 100644 --- a/api/api.go +++ b/api/api.go @@ -1,8 +1,11 @@ package api import ( + "bytes" "encoding/hex" + "errors" "fmt" + "io" "net/http" "reflect" "time" @@ -39,8 +42,12 @@ func CreateServer( rateLimitTimeWindowInSeconds int, isProfileModeActivated bool, shouldStartSwaggerUI bool, + maxRequestBodySize int64, ) (*http.Server, error) { ws := gin.Default() + if maxRequestBodySize > 0 { + ws.Use(maxRequestBodySizeMiddleware(maxRequestBodySize)) + } ws.Use(cors.Default()) err := registerValidators() @@ -61,6 +68,40 @@ func CreateServer( return httpServer, nil } +func maxRequestBodySizeMiddleware(maxSize int64) gin.HandlerFunc { + return func(c *gin.Context) { + if c.Request.Body == nil { + c.Next() + return + } + + bodyBytes, err := io.ReadAll(http.MaxBytesReader(c.Writer, c.Request.Body, maxSize)) + if err != nil { + if maxBytesErr, ok := errors.AsType[*http.MaxBytesError](err); ok { + printMessage := fmt.Sprintf("request body exceeded the size limit of %d bytes", maxBytesErr.Limit) + log.Warn(printMessage, "path", c.Request.RequestURI) + c.AbortWithStatusJSON(http.StatusRequestEntityTooLarge, data.GenericAPIResponse{ + Data: nil, + Error: printMessage, + Code: data.ReturnCodeRequestError, + }) + return + } + + log.Warn("error reading request body", "error", err, "path", c.Request.RequestURI) + c.AbortWithStatusJSON(http.StatusBadRequest, data.GenericAPIResponse{ + Data: nil, + Error: err.Error(), + Code: data.ReturnCodeRequestError, + }) + return + } + + c.Request.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) + c.Next() + } +} + func registerValidators() error { validators := []validatorInput{ {Name: "skValidator", Validator: skValidator}, diff --git a/api/api_test.go b/api/api_test.go new file mode 100644 index 00000000..95529377 --- /dev/null +++ b/api/api_test.go @@ -0,0 +1,118 @@ +package api + +import ( + "bytes" + "io" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" + + "github.com/gin-gonic/gin" + "github.com/multiversx/mx-chain-proxy-go/api/middleware" + "github.com/stretchr/testify/require" +) + +func TestMaxRequestBodySizeMiddleware_OversizedBodyShouldErr(t *testing.T) { + t.Parallel() + + ws := gin.New() + ws.Use(maxRequestBodySizeMiddleware(1 << 20)) + ws.POST("/test", func(c *gin.Context) { + _, err := io.ReadAll(c.Request.Body) + if err != nil { + c.Status(http.StatusRequestEntityTooLarge) + return + } + c.Status(http.StatusOK) + }) + + hugeBody := make([]byte, 2*1024*1024) + for i := range hugeBody { + hugeBody[i] = 'a' + } + + req := httptest.NewRequest(http.MethodPost, "/test", bytes.NewReader(hugeBody)) + resp := httptest.NewRecorder() + ws.ServeHTTP(resp, req) + + require.Equal(t, http.StatusRequestEntityTooLarge, resp.Code) +} + +func TestMaxRequestBodySizeMiddleware_NormalBodyShouldPass(t *testing.T) { + t.Parallel() + + ws := gin.New() + ws.Use(maxRequestBodySizeMiddleware(1 << 20)) + ws.POST("/test", func(c *gin.Context) { + body, err := io.ReadAll(c.Request.Body) + if err != nil { + c.Status(http.StatusInternalServerError) + return + } + if string(body) != `{"key":"value"}` { + c.Status(http.StatusBadRequest) + return + } + c.Status(http.StatusOK) + }) + + body := []byte(`{"key":"value"}`) + req := httptest.NewRequest(http.MethodPost, "/test", bytes.NewReader(body)) + resp := httptest.NewRecorder() + ws.ServeHTTP(resp, req) + + require.Equal(t, http.StatusOK, resp.Code) +} + +func TestMaxRequestBodySizeMiddleware_LoggingDisabled_ValidJSONWithExcessWhitespaceShouldBeRejected(t *testing.T) { + t.Parallel() + + validJSON := `{"valid":"request"}` + limit := int64(len(validJSON)) + + ws := gin.New() + ws.Use(maxRequestBodySizeMiddleware(limit)) + ws.POST("/test", func(c *gin.Context) { + var payload map[string]string + if err := c.ShouldBindJSON(&payload); err != nil { + c.Status(http.StatusBadRequest) + return + } + c.Status(http.StatusOK) + }) + + body := validJSON + strings.Repeat(" ", 10) + req := httptest.NewRequest(http.MethodPost, "/test", strings.NewReader(body)) + resp := httptest.NewRecorder() + ws.ServeHTTP(resp, req) + + require.Equal(t, http.StatusRequestEntityTooLarge, resp.Code) +} + +func TestMaxRequestBodySizeMiddleware_LoggingEnabled_ValidJSONWithExcessWhitespaceShouldBeRejected(t *testing.T) { + t.Parallel() + + validJSON := `{"valid":"request"}` + limit := int64(len(validJSON)) + + ws := gin.New() + ws.Use(maxRequestBodySizeMiddleware(limit)) + ws.Use(middleware.NewResponseLoggerMiddleware(10 * time.Second).MiddlewareHandlerFunc()) + ws.POST("/test", func(c *gin.Context) { + var payload map[string]string + if err := c.ShouldBindJSON(&payload); err != nil { + c.Status(http.StatusBadRequest) + return + } + c.Status(http.StatusOK) + }) + + body := validJSON + strings.Repeat(" ", 10) + req := httptest.NewRequest(http.MethodPost, "/test", strings.NewReader(body)) + resp := httptest.NewRecorder() + ws.ServeHTTP(resp, req) + + require.Equal(t, http.StatusRequestEntityTooLarge, resp.Code) +} diff --git a/api/middleware/responseLogger.go b/api/middleware/responseLogger.go index 64df1010..2074ed9e 100644 --- a/api/middleware/responseLogger.go +++ b/api/middleware/responseLogger.go @@ -3,7 +3,7 @@ package middleware import ( "bytes" "fmt" - "io/ioutil" + "io" "net/http" "strings" "time" @@ -47,10 +47,17 @@ func (rlm *responseLoggerMiddleware) MiddlewareHandlerFunc() gin.HandlerFunc { // read the body for logging purposes and restore it into the context var bodyBytes []byte + var err error if c.Request.Body != nil { - bodyBytes, _ = ioutil.ReadAll(c.Request.Body) + bodyBytes, err = io.ReadAll(c.Request.Body) + if err != nil { + log.Warn("error reading request body", "error", err) + } + } + if err == nil { + // the body was read successfully, so it can be safely restored for the handlers + c.Request.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) } - c.Request.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes)) requestBodyString := string(bodyBytes) bw := &bodyWriter{body: bytes.NewBufferString(""), ResponseWriter: c.Writer} diff --git a/api/middleware/responseLogger_test.go b/api/middleware/responseLogger_test.go index c3595b5f..91684f70 100644 --- a/api/middleware/responseLogger_test.go +++ b/api/middleware/responseLogger_test.go @@ -3,6 +3,7 @@ package middleware import ( "errors" "fmt" + "io" "net/http" "net/http/httptest" "strings" @@ -163,3 +164,27 @@ func TestResponseLoggerMiddleware_ShouldNotCallHandler(t *testing.T) { assert.Equal(t, http.StatusOK, resp.Code) assert.False(t, handlerWasCalled) } + +func TestResponseLoggerMiddleware_OversizedBodyShouldNotBeAccepted(t *testing.T) { + t.Parallel() + + limit := int64(10) + ws := gin.New() + ws.Use(NewResponseLoggerMiddleware(10 * time.Second).MiddlewareHandlerFunc()) + ws.POST("/test", func(c *gin.Context) { + _, err := io.ReadAll(c.Request.Body) + if err != nil { + c.Status(http.StatusRequestEntityTooLarge) + return + } + c.Status(http.StatusOK) + }) + + req := httptest.NewRequest(http.MethodPost, "/test", strings.NewReader("ABCDEFGHIJK")) + req.Body = http.MaxBytesReader(httptest.NewRecorder(), req.Body, limit) + + resp := httptest.NewRecorder() + ws.ServeHTTP(resp, req) + + assert.Equal(t, http.StatusRequestEntityTooLarge, resp.Code) +} diff --git a/cmd/proxy/config/config.toml b/cmd/proxy/config/config.toml index 1a4e95e9..100a5c25 100644 --- a/cmd/proxy/config/config.toml +++ b/cmd/proxy/config/config.toml @@ -48,6 +48,9 @@ # TimeBetweenNodesRequestsInSec represents time to wait before retry to get the number of shards from observers TimeBetweenNodesRequestsInSec = 2 + # MaxRequestBodySize represents the maximum request body size in bytes for all API endpoints + MaxRequestBodySize = 10000000 # 10 MB + [AddressPubkeyConverter] #Length specifies the length in bytes of an address Length = 32 diff --git a/cmd/proxy/main.go b/cmd/proxy/main.go index 311bc2a9..d71ab7f9 100644 --- a/cmd/proxy/main.go +++ b/cmd/proxy/main.go @@ -611,6 +611,7 @@ func startWebServer( generalConfig.GeneralSettings.RateLimitWindowDurationSeconds, isProfileModeActivated, shouldStartSwaggerUI, + generalConfig.GeneralSettings.MaxRequestBodySize, ) if err != nil { diff --git a/config/config.go b/config/config.go index 0cfcc4de..b83e877c 100644 --- a/config/config.go +++ b/config/config.go @@ -19,6 +19,7 @@ type GeneralSettingsConfig struct { AllowEntireTxPoolFetch bool NumShardsTimeoutInSec int TimeBetweenNodesRequestsInSec int + MaxRequestBodySize int64 } // Config will hold the whole config file's data diff --git a/data/transaction.go b/data/transaction.go index d5d4d02b..78cb3345 100644 --- a/data/transaction.go +++ b/data/transaction.go @@ -127,6 +127,7 @@ type TransactionSimulationResults struct { ScResults map[string]*transaction.ApiSmartContractResult `json:"scResults,omitempty"` Receipts map[string]*transaction.ApiReceipt `json:"receipts,omitempty"` Hash string `json:"hash,omitempty"` + Logs *transaction.ApiLogs `json:"logs,omitempty"` } // TransactionSimulationResponseData represents the format of the data field of a transaction simulation response diff --git a/process/scQueryProcessor.go b/process/scQueryProcessor.go index 915a0921..5c1939b0 100644 --- a/process/scQueryProcessor.go +++ b/process/scQueryProcessor.go @@ -59,10 +59,10 @@ func (scQueryProcessor *SCQueryProcessor) ExecuteQuery(query *data.SCQuery) (*vm return nil, data.BlockInfo{}, err } + request := scQueryProcessor.createRequestFromQuery(query) + response := data.ResponseVmValue{} for _, observer := range observers { - request := scQueryProcessor.createRequestFromQuery(query) - params := url.Values{} if query.BlockNonce.HasValue { params.Add(blockNonce, fmt.Sprintf("%d", query.BlockNonce.Value))