Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: allow to disable file uploads #896

Merged
merged 2 commits into from
Jun 26, 2024
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
33 changes: 25 additions & 8 deletions router-tests/file_upload_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,10 @@ func TestSingleFileUpload_NoFileProvided(t *testing.T) {
func TestFileUpload_FilesSizeExceedsLimit(t *testing.T) {
t.Parallel()
testenv.Run(t, &testenv.Config{
RouterOptions: []core.Option{core.WithRouterTrafficConfig(&config.RouterTrafficConfiguration{
MaxUploadFiles: 1,
MaxRequestBodyBytes: 100,
MaxUploadFileSizeBytes: 50,
RouterOptions: []core.Option{core.WithFileUploadConfig(&config.FileUpload{
Enabled: true,
MaxFiles: 1,
MaxFileSizeBytes: 50,
})},
}, func(t *testing.T, xEnv *testenv.Environment) {
files := make([][]byte, 1)
Expand All @@ -73,10 +73,10 @@ func TestFileUpload_FilesSizeExceedsLimit(t *testing.T) {
func TestFileUpload_FilesExceedsLimit(t *testing.T) {
t.Parallel()
testenv.Run(t, &testenv.Config{
RouterOptions: []core.Option{core.WithRouterTrafficConfig(&config.RouterTrafficConfiguration{
MaxUploadFiles: 2,
MaxRequestBodyBytes: 50000,
MaxUploadFileSizeBytes: 50000,
RouterOptions: []core.Option{core.WithFileUploadConfig(&config.FileUpload{
Enabled: true,
MaxFiles: 2,
MaxFileSizeBytes: 50000,
})},
}, func(t *testing.T, xEnv *testenv.Environment) {
files := make([][]byte, 3)
Expand Down Expand Up @@ -129,3 +129,20 @@ func TestMultipleFilesUpload_NoFilesProvided(t *testing.T) {
require.JSONEq(t, `{"errors":[{"message":"Failed to fetch from Subgraph '0' at Path 'mutation'.","extensions":{"errors":[{"message":"could not render fetch input","path":[]}]}},{"message":"Cannot return null for non-nullable field 'Mutation.multipleUpload'.","path":["multipleUpload"]}],"data":null}`, res.Body)
})
}

func TestFileUpload_UploadDisabled(t *testing.T) {
t.Parallel()
testenv.Run(t, &testenv.Config{
RouterOptions: []core.Option{core.WithFileUploadConfig(&config.FileUpload{
Enabled: false,
})},
}, func(t *testing.T, xEnv *testenv.Environment) {
files := make([][]byte, 1)
res := xEnv.MakeGraphQLRequestOK(testenv.GraphQLRequest{
Query: "mutation($files: [Upload!]!) { multipleUpload(files: $files)}",
Variables: []byte(`{"files":[null]}`),
Files: files,
})
require.Equal(t, `{"errors":[{"message":"file upload disabled"}],"data":null}`, res.Body)
})
}
1 change: 1 addition & 0 deletions router/cmd/instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ func NewRouter(params Params, additionalOptions ...core.Option) (*core.Router, e
core.WithHeaderRules(cfg.Headers),
core.WithStaticRouterConfig(routerConfig),
core.WithRouterTrafficConfig(&cfg.TrafficShaping.Router),
core.WithFileUploadConfig(&cfg.FileUpload),
core.WithSubgraphTransportOptions(&core.SubgraphTransportOptions{
RequestTimeout: cfg.TrafficShaping.All.RequestTimeout,
ResponseHeaderTimeout: cfg.TrafficShaping.All.ResponseHeaderTimeout,
Expand Down
15 changes: 14 additions & 1 deletion router/core/graphql_prehandler.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ type PreHandlerOptions struct {
FlushTelemetryAfterResponse bool
TraceExportVariables bool
SpanAttributesMapper func(r *http.Request) []attribute.KeyValue
FileUploadEnabled bool
MaxUploadFiles int
MaxUploadFileSize int
}
Expand All @@ -67,6 +68,7 @@ type PreHandler struct {
tracer trace.Tracer
traceExportVariables bool
spanAttributesMapper func(r *http.Request) []attribute.KeyValue
fileUploadEnabled bool
maxUploadFiles int
maxUploadFileSize int
}
Expand All @@ -91,6 +93,7 @@ func NewPreHandler(opts *PreHandlerOptions) *PreHandler {
"wundergraph/cosmo/router/pre_handler",
trace.WithInstrumentationVersion("0.0.1"),
),
fileUploadEnabled: opts.FileUploadEnabled,
maxUploadFiles: opts.MaxUploadFiles,
maxUploadFileSize: opts.MaxUploadFileSize,
}
Expand Down Expand Up @@ -177,12 +180,12 @@ func (h *PreHandler) Handler(next http.Handler) http.Handler {

var body []byte
var files []httpclient.File
var err error
// XXX: This buffer needs to be returned to the pool only
// AFTER we're done with body (retrieved from parser.ReadBody())
buf := pool.GetBytesBuffer()
defer pool.PutBytesBuffer(buf)
if r.Header.Get("Content-Type") == "" || r.Header.Get("Content-Type") == "application/json" {
var err error
body, err = h.operationProcessor.ReadBody(buf, r.Body)
if err != nil {
finalErr = err
Expand All @@ -199,8 +202,18 @@ func (h *PreHandler) Handler(next http.Handler) http.Handler {
return
}
} else if strings.Contains(r.Header.Get("Content-Type"), "multipart/form-data") {
if !h.fileUploadEnabled {
finalErr = &inputError{
message: "file upload disabled",
statusCode: http.StatusOK,
}
writeOperationError(r, w, requestLogger, finalErr)
return
}

multipartParser := NewMultipartParser(h.operationProcessor, h.maxUploadFiles, h.maxUploadFileSize)

var err error
body, files, err = multipartParser.Parse(r, buf)
if err != nil {
finalErr = err
Expand Down
27 changes: 22 additions & 5 deletions router/core/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ type (
subgraphTransportOptions *SubgraphTransportOptions
graphqlMetricsConfig *GraphQLMetricsConfig
routerTrafficConfig *config.RouterTrafficConfiguration
fileUploadConfig *config.FileUpload
accessController *AccessController
retryOptions retrytransport.RetryOptions
redisClient *redis.Client
Expand Down Expand Up @@ -303,6 +304,9 @@ func NewRouter(opts ...Option) (*Router, error) {
if r.routerTrafficConfig == nil {
r.routerTrafficConfig = DefaultRouterTrafficConfig()
}
if r.fileUploadConfig == nil {
r.fileUploadConfig = DefaultFileUploadConfig()
}
if r.accessController == nil {
r.accessController = DefaultAccessController()
} else {
Expand Down Expand Up @@ -1342,8 +1346,9 @@ func (r *Router) newServer(ctx context.Context, routerConfig *nodev1.RouterConfi
FlushTelemetryAfterResponse: r.awsLambda,
TraceExportVariables: r.traceConfig.ExportGraphQLVariables.Enabled,
SpanAttributesMapper: r.traceConfig.SpanAttributesMapper,
MaxUploadFiles: r.routerTrafficConfig.MaxUploadFiles,
MaxUploadFileSize: int(r.routerTrafficConfig.MaxUploadFileSizeBytes),
FileUploadEnabled: r.fileUploadConfig.Enabled,
MaxUploadFiles: r.fileUploadConfig.MaxFiles,
MaxUploadFileSize: int(r.fileUploadConfig.MaxFileSizeBytes),
})

graphqlChiRouter := chi.NewRouter()
Expand Down Expand Up @@ -1820,6 +1825,12 @@ func WithRouterTrafficConfig(cfg *config.RouterTrafficConfiguration) Option {
}
}

func WithFileUploadConfig(cfg *config.FileUpload) Option {
return func(r *Router) {
r.fileUploadConfig = cfg
}
}

func WithAccessController(controller *AccessController) Option {
return func(r *Router) {
r.accessController = controller
Expand All @@ -1846,9 +1857,15 @@ func WithLocalhostFallbackInsideDocker(fallback bool) Option {

func DefaultRouterTrafficConfig() *config.RouterTrafficConfiguration {
return &config.RouterTrafficConfiguration{
MaxRequestBodyBytes: 1000 * 1000 * 5, // 5 MB
MaxUploadFileSizeBytes: 1000 * 1000 * 50, // 50 MB,
MaxUploadFiles: 10,
MaxRequestBodyBytes: 1000 * 1000 * 5, // 5 MB
}
}

func DefaultFileUploadConfig() *config.FileUpload {
return &config.FileUpload{
Enabled: true,
MaxFileSizeBytes: 1000 * 1000 * 50, // 50 MB,
MaxFiles: 10,
}
}

Expand Down
11 changes: 8 additions & 3 deletions router/pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,11 +124,15 @@ type TrafficShapingRules struct {
Router RouterTrafficConfiguration `yaml:"router"`
}

type FileUpload struct {
Enabled bool `yaml:"enabled" default:"true" envconfig:"FILE_UPLOAD_ENABLED"`
MaxFileSizeBytes BytesString `yaml:"max_file_size" default:"50MB" envconfig:"FILE_UPLOAD_MAX_FILE_SIZE"`
MaxFiles int `yaml:"max_files" default:"10" envconfig:"FILE_UPLOAD_MAX_FILES"`
}

type RouterTrafficConfiguration struct {
// MaxRequestBodyBytes is the maximum size of the request body in bytes
MaxRequestBodyBytes BytesString `yaml:"max_request_body_size" default:"5MB"`
MaxUploadFileSizeBytes BytesString `yaml:"max_upload_file_size" default:"50MB"`
MaxUploadFiles int `yaml:"max_upload_files" default:"10"`
MaxRequestBodyBytes BytesString `yaml:"max_request_body_size" default:"5MB"`
}

type GlobalSubgraphRequestRule struct {
Expand Down Expand Up @@ -426,6 +430,7 @@ type Config struct {
Modules map[string]interface{} `yaml:"modules,omitempty"`
Headers HeaderRules `yaml:"headers,omitempty"`
TrafficShaping TrafficShapingRules `yaml:"traffic_shaping,omitempty"`
FileUpload FileUpload `yaml:"file_upload,omitempty"`

ListenAddr string `yaml:"listen_addr" default:"localhost:3002" envconfig:"LISTEN_ADDR"`
ControlplaneURL string `yaml:"controlplane_url" default:"https://cosmo-cp.wundergraph.com" envconfig:"CONTROLPLANE_URL"`
Expand Down
37 changes: 24 additions & 13 deletions router/pkg/config/config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -624,6 +624,30 @@
"default": "/",
"description": "The path of the GraphQL Playground. The GraphQL Playground is a web-based GraphQL IDE that allows you to interact with the GraphQL API. The default value is '/'."
},
"file_upload": {
"type": "object",
"description": "The configuration for file upload. Configure whether it should be enabled along with file size and number of files.",
"additionalProperties": false,
"properties": {
"enabled": {
"type": "boolean",
"default": true
},
"max_file_size": {
"type": "string",
"bytes": {
"minimum": "1MB"
},
"description": "The maximum size of a file that can be uploaded. The size is specified as a string with a number and a unit, e.g. 10KB, 1MB, 1GB. The supported units are 'KB', 'MB', 'GB'."
},
"max_files": {
"type": "integer",
"default": 10,
"minimum": 1,
"description": "The maximum number of files that can be uploaded."
}
}
},
"traffic_shaping": {
"type": "object",
"description": "The configuration for the traffic shaping. Configure rules for traffic shaping like maximum request body size, timeouts, retry behavior, etc. See https://cosmo-docs.wundergraph.com/router/traffic-shaping for more information.",
Expand All @@ -639,19 +663,6 @@
"minimum": "1MB"
},
"description": "The maximum request body size. The size is specified as a string with a number and a unit, e.g. 10KB, 1MB, 1GB. The supported units are 'KB', 'MB', 'GB'."
},
"max_upload_file_size": {
"type": "string",
"bytes": {
"minimum": "1MB"
},
"description": "The maximum size of a file that can be uploaded. The size is specified as a string with a number and a unit, e.g. 10KB, 1MB, 1GB. The supported units are 'KB', 'MB', 'GB'."
},
"max_upload_files": {
"type": "integer",
"default": 10,
"minimum": 1,
"description": "The maximum number of files that can be uploaded."
}
}
},
Expand Down
9 changes: 6 additions & 3 deletions router/pkg/config/testdata/config_defaults.json
Original file line number Diff line number Diff line change
Expand Up @@ -104,11 +104,14 @@
"KeepAliveProbeInterval": 30000000000
},
"Router": {
"MaxRequestBodyBytes": 5000000,
"MaxUploadFileSizeBytes": 50000000,
"MaxUploadFiles": 10
"MaxRequestBodyBytes": 5000000
}
},
"FileUpload": {
"Enabled": true,
"MaxFileSizeBytes": 50000000,
"MaxFiles": 10
},
"ListenAddr": "localhost:3002",
"ControlplaneURL": "https://cosmo-cp.wundergraph.com",
"PlaygroundEnabled": true,
Expand Down
9 changes: 6 additions & 3 deletions router/pkg/config/testdata/config_full.json
Original file line number Diff line number Diff line change
Expand Up @@ -160,11 +160,14 @@
"KeepAliveProbeInterval": 30000000000
},
"Router": {
"MaxRequestBodyBytes": 5000000,
"MaxUploadFileSizeBytes": 50000000,
"MaxUploadFiles": 10
"MaxRequestBodyBytes": 5000000
}
},
"FileUpload": {
"Enabled": true,
"MaxFileSizeBytes": 50000000,
"MaxFiles": 10
},
"ListenAddr": "localhost:3002",
"ControlplaneURL": "https://cosmo-cp.wundergraph.com",
"PlaygroundEnabled": true,
Expand Down
Loading