node, rpc: add configurable HTTP request limit

This commit is contained in:
Felix Lange 2024-02-07 16:54:43 +01:00
parent e2778cd59f
commit 8fe5ace9e8
6 changed files with 35 additions and 14 deletions

View file

@ -41,6 +41,7 @@ const (
// needs of all CLs. // needs of all CLs.
engineAPIBatchItemLimit = 2000 engineAPIBatchItemLimit = 2000
engineAPIBatchResponseSizeLimit = 250 * 1000 * 1000 engineAPIBatchResponseSizeLimit = 250 * 1000 * 1000
engineAPIBodyLimit = 128 * 1024 * 1024
) )
var ( var (

View file

@ -453,14 +453,16 @@ func (n *Node) startRPC() error {
jwtSecret: secret, jwtSecret: secret,
batchItemLimit: engineAPIBatchItemLimit, batchItemLimit: engineAPIBatchItemLimit,
batchResponseSizeLimit: engineAPIBatchResponseSizeLimit, batchResponseSizeLimit: engineAPIBatchResponseSizeLimit,
httpBodyLimit: engineAPIBodyLimit,
} }
if err := server.enableRPC(allAPIs, httpConfig{ err := server.enableRPC(allAPIs, httpConfig{
CorsAllowedOrigins: DefaultAuthCors, CorsAllowedOrigins: DefaultAuthCors,
Vhosts: n.config.AuthVirtualHosts, Vhosts: n.config.AuthVirtualHosts,
Modules: DefaultAuthModules, Modules: DefaultAuthModules,
prefix: DefaultAuthPrefix, prefix: DefaultAuthPrefix,
rpcEndpointConfig: sharedConfig, rpcEndpointConfig: sharedConfig,
}); err != nil { })
if err != nil {
return err return err
} }
servers = append(servers, server) servers = append(servers, server)

View file

@ -56,6 +56,7 @@ type rpcEndpointConfig struct {
jwtSecret []byte // optional JWT secret jwtSecret []byte // optional JWT secret
batchItemLimit int batchItemLimit int
batchResponseSizeLimit int batchResponseSizeLimit int
httpBodyLimit int
} }
type rpcHandler struct { type rpcHandler struct {
@ -304,6 +305,9 @@ func (h *httpServer) enableRPC(apis []rpc.API, config httpConfig) error {
// Create RPC server and handler. // Create RPC server and handler.
srv := rpc.NewServer() srv := rpc.NewServer()
srv.SetBatchLimits(config.batchItemLimit, config.batchResponseSizeLimit) srv.SetBatchLimits(config.batchItemLimit, config.batchResponseSizeLimit)
if config.httpBodyLimit > 0 {
srv.SetHTTPBodyLimit(config.httpBodyLimit)
}
if err := RegisterApis(apis, config.Modules, srv); err != nil { if err := RegisterApis(apis, config.Modules, srv); err != nil {
return err return err
} }
@ -336,6 +340,9 @@ func (h *httpServer) enableWS(apis []rpc.API, config wsConfig) error {
// Create RPC server and handler. // Create RPC server and handler.
srv := rpc.NewServer() srv := rpc.NewServer()
srv.SetBatchLimits(config.batchItemLimit, config.batchResponseSizeLimit) srv.SetBatchLimits(config.batchItemLimit, config.batchResponseSizeLimit)
if config.httpBodyLimit > 0 {
srv.SetHTTPBodyLimit(config.httpBodyLimit)
}
if err := RegisterApis(apis, config.Modules, srv); err != nil { if err := RegisterApis(apis, config.Modules, srv); err != nil {
return err return err
} }

View file

@ -33,7 +33,7 @@ import (
) )
const ( const (
maxRequestContentLength = 1024 * 1024 * 5 defaultBodyLimit = 5 * 1024 * 1024
contentType = "application/json" contentType = "application/json"
) )
@ -253,8 +253,8 @@ type httpServerConn struct {
r *http.Request r *http.Request
} }
func newHTTPServerConn(r *http.Request, w http.ResponseWriter) ServerCodec { func (s *Server) newHTTPServerConn(r *http.Request, w http.ResponseWriter) ServerCodec {
body := io.LimitReader(r.Body, maxRequestContentLength) body := io.LimitReader(r.Body, int64(s.httpBodyLimit))
conn := &httpServerConn{Reader: body, Writer: w, r: r} conn := &httpServerConn{Reader: body, Writer: w, r: r}
encoder := func(v any, isErrorResponse bool) error { encoder := func(v any, isErrorResponse bool) error {
@ -312,7 +312,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
return return
} }
if code, err := validateRequest(r); err != nil { if code, err := s.validateRequest(r); err != nil {
http.Error(w, err.Error(), code) http.Error(w, err.Error(), code)
return return
} }
@ -330,19 +330,19 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// until EOF, writes the response to w, and orders the server to process a // until EOF, writes the response to w, and orders the server to process a
// single request. // single request.
w.Header().Set("content-type", contentType) w.Header().Set("content-type", contentType)
codec := newHTTPServerConn(r, w) codec := s.newHTTPServerConn(r, w)
defer codec.close() defer codec.close()
s.serveSingleRequest(ctx, codec) s.serveSingleRequest(ctx, codec)
} }
// validateRequest returns a non-zero response code and error message if the // validateRequest returns a non-zero response code and error message if the
// request is invalid. // request is invalid.
func validateRequest(r *http.Request) (int, error) { func (s *Server) validateRequest(r *http.Request) (int, error) {
if r.Method == http.MethodPut || r.Method == http.MethodDelete { if r.Method == http.MethodPut || r.Method == http.MethodDelete {
return http.StatusMethodNotAllowed, errors.New("method not allowed") return http.StatusMethodNotAllowed, errors.New("method not allowed")
} }
if r.ContentLength > maxRequestContentLength { if r.ContentLength > int64(s.httpBodyLimit) {
err := fmt.Errorf("content length too large (%d>%d)", r.ContentLength, maxRequestContentLength) err := fmt.Errorf("content length too large (%d>%d)", r.ContentLength, s.httpBodyLimit)
return http.StatusRequestEntityTooLarge, err return http.StatusRequestEntityTooLarge, err
} }
// Allow OPTIONS (regardless of content-type) // Allow OPTIONS (regardless of content-type)

View file

@ -40,11 +40,13 @@ func confirmStatusCode(t *testing.T, got, want int) {
func confirmRequestValidationCode(t *testing.T, method, contentType, body string, expectedStatusCode int) { func confirmRequestValidationCode(t *testing.T, method, contentType, body string, expectedStatusCode int) {
t.Helper() t.Helper()
s := NewServer()
request := httptest.NewRequest(method, "http://url.com", strings.NewReader(body)) request := httptest.NewRequest(method, "http://url.com", strings.NewReader(body))
if len(contentType) > 0 { if len(contentType) > 0 {
request.Header.Set("Content-Type", contentType) request.Header.Set("Content-Type", contentType)
} }
code, err := validateRequest(request) code, err := s.validateRequest(request)
if code == 0 { if code == 0 {
if err != nil { if err != nil {
t.Errorf("validation: got error %v, expected nil", err) t.Errorf("validation: got error %v, expected nil", err)

View file

@ -51,6 +51,7 @@ type Server struct {
run atomic.Bool run atomic.Bool
batchItemLimit int batchItemLimit int
batchResponseLimit int batchResponseLimit int
httpBodyLimit int
} }
// NewServer creates a new server instance with no registered handlers. // NewServer creates a new server instance with no registered handlers.
@ -58,6 +59,7 @@ func NewServer() *Server {
server := &Server{ server := &Server{
idgen: randomIDGenerator(), idgen: randomIDGenerator(),
codecs: make(map[ServerCodec]struct{}), codecs: make(map[ServerCodec]struct{}),
httpBodyLimit: defaultBodyLimit,
} }
server.run.Store(true) server.run.Store(true)
// Register the default service providing meta information about the RPC service such // Register the default service providing meta information about the RPC service such
@ -78,6 +80,13 @@ func (s *Server) SetBatchLimits(itemLimit, maxResponseSize int) {
s.batchResponseLimit = maxResponseSize s.batchResponseLimit = maxResponseSize
} }
// SetHTTPBodyLimit sets the size limit for HTTP requests.
//
// This method should be called before processing any requests via ServeHTTP.
func (s *Server) SetHTTPBodyLimit(limit int) {
s.httpBodyLimit = limit
}
// RegisterName creates a service for the given receiver type under the given name. When no // RegisterName creates a service for the given receiver type under the given name. When no
// methods on the given receiver match the criteria to be either a RPC method or a // methods on the given receiver match the criteria to be either a RPC method or a
// subscription an error is returned. Otherwise a new service is created and added to the // subscription an error is returned. Otherwise a new service is created and added to the