Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions api/api.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package api

import (
"bytes"
"encoding/hex"
"errors"
"fmt"
"io"
"net/http"
"reflect"
"time"
Expand Down Expand Up @@ -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()
Expand All @@ -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},
Expand Down
118 changes: 118 additions & 0 deletions api/api_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
13 changes: 10 additions & 3 deletions api/middleware/responseLogger.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package middleware
import (
"bytes"
"fmt"
"io/ioutil"
"io"
"net/http"
"strings"
"time"
Expand Down Expand Up @@ -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}
Expand Down
25 changes: 25 additions & 0 deletions api/middleware/responseLogger_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package middleware
import (
"errors"
"fmt"
"io"
"net/http"
"net/http/httptest"
"strings"
Expand Down Expand Up @@ -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)
}
3 changes: 3 additions & 0 deletions cmd/proxy/config/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions cmd/proxy/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -611,6 +611,7 @@ func startWebServer(
generalConfig.GeneralSettings.RateLimitWindowDurationSeconds,
isProfileModeActivated,
shouldStartSwaggerUI,
generalConfig.GeneralSettings.MaxRequestBodySize,
)

if err != nil {
Expand Down
1 change: 1 addition & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ type GeneralSettingsConfig struct {
AllowEntireTxPoolFetch bool
NumShardsTimeoutInSec int
TimeBetweenNodesRequestsInSec int
MaxRequestBodySize int64
}

// Config will hold the whole config file's data
Expand Down
1 change: 1 addition & 0 deletions data/transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions process/scQueryProcessor.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
Loading