From a1e1fdfbd78d50df108e98c2ad5a6b6477ece3ff Mon Sep 17 00:00:00 2001 From: Marius Date: Wed, 6 Sep 2023 11:01:19 +0200 Subject: [PATCH] cli, handler: Allow to customize CORS handling (#997) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: Add flag to control CORS Origin header (#987) * added flags for CORS header * go fmt * fix: go vet * fix: check Origin to match configured CORS Origin --------- Co-authored-by: Sean Macdonald * handler: Restrict method overriding to POST requests * handler: Implement more CORS options * Add back support for DisableCors * Add flags for CORS options * Add documentation * Fix flag description * Remove now unneeded -cors-origin flag --------- Co-authored-by: Thomas Müller <1005065+DeepDiver1975@users.noreply.github.com> Co-authored-by: Sean Macdonald --- cmd/tusd/cli/flags.go | 12 ++ cmd/tusd/cli/serve.go | 30 ++++- pkg/handler/config.go | 59 ++++++++ pkg/handler/cors_test.go | 231 ++++++++++++++++++++++++++------ pkg/handler/unrouted_handler.go | 39 +++--- 5 files changed, 313 insertions(+), 58 deletions(-) diff --git a/cmd/tusd/cli/flags.go b/cmd/tusd/cli/flags.go index 1cc1e4b45..679972909 100644 --- a/cmd/tusd/cli/flags.go +++ b/cmd/tusd/cli/flags.go @@ -24,6 +24,12 @@ var Flags struct { DisableDownload bool DisableTermination bool DisableCors bool + CorsAllowOrigin string + CorsAllowCredentials bool + CorsAllowMethods string + CorsAllowHeaders string + CorsMaxAge string + CorsExposeHeaders string Timeout int64 S3Bucket string S3ObjectPrefix string @@ -75,6 +81,12 @@ func ParseFlags() { flag.BoolVar(&Flags.DisableDownload, "disable-download", false, "Disable the download endpoint") flag.BoolVar(&Flags.DisableTermination, "disable-termination", false, "Disable the termination endpoint") flag.BoolVar(&Flags.DisableCors, "disable-cors", false, "Disable CORS headers") + flag.StringVar(&Flags.CorsAllowOrigin, "cors-allow-origin", ".*", "Regular expression used to determine if the Origin header is allowed. If not, no CORS headers will be sent. By default, all origins are allowed.") + flag.BoolVar(&Flags.CorsAllowCredentials, "cors-allow-credentials", false, "Allow credentials by setting Access-Control-Allow-Credentials: true") + flag.StringVar(&Flags.CorsAllowMethods, "cors-allow-methods", "", "Comma-separated list of request methods that are included in Access-Control-Allow-Methods in addition to the ones required by tusd") + flag.StringVar(&Flags.CorsAllowHeaders, "cors-allow-headers", "", "Comma-separated list of headers that are included in Access-Control-Allow-Headers in addition to the ones required by tusd") + flag.StringVar(&Flags.CorsMaxAge, "cors-max-age", "86400", "Value of the Access-Control-Max-Age header to control the cache duration of CORS responses.") + flag.StringVar(&Flags.CorsExposeHeaders, "cors-expose-headers", "", "Comma-separated list of headers that are included in Access-Control-Expose-Headers in addition to the ones required by tusd") flag.Int64Var(&Flags.Timeout, "timeout", 6*1000, "Read timeout for connections in milliseconds. A zero value means that reads will not timeout") flag.StringVar(&Flags.S3Bucket, "s3-bucket", "", "Use AWS S3 with this bucket as storage backend (requires the AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY and AWS_REGION environment variables to be set)") flag.StringVar(&Flags.S3ObjectPrefix, "s3-object-prefix", "", "Prefix for S3 object names") diff --git a/cmd/tusd/cli/serve.go b/cmd/tusd/cli/serve.go index b4619ccbc..868881632 100644 --- a/cmd/tusd/cli/serve.go +++ b/cmd/tusd/cli/serve.go @@ -4,6 +4,7 @@ import ( "crypto/tls" "net" "net/http" + "regexp" "strings" "time" @@ -26,11 +27,11 @@ func Serve() { config := handler.Config{ MaxSize: Flags.MaxSize, BasePath: Flags.Basepath, + Cors: getCorsConfig(), RespectForwardedHeaders: Flags.BehindProxy, EnableExperimentalProtocol: Flags.ExperimentalProtocol, DisableDownload: Flags.DisableDownload, DisableTermination: Flags.DisableTermination, - DisableCors: Flags.DisableCors, StoreComposer: Composer, NotifyCompleteUploads: true, NotifyTerminatedUploads: true, @@ -166,3 +167,30 @@ func Serve() { stderr.Fatalf("Unable to serve: %s", err) } } + +func getCorsConfig() *handler.CorsConfig { + config := handler.DefaultCorsConfig + config.Disable = Flags.DisableCors + config.AllowCredentials = Flags.CorsAllowCredentials + config.MaxAge = Flags.CorsMaxAge + + var err error + config.AllowOrigin, err = regexp.Compile(Flags.CorsAllowOrigin) + if err != nil { + stderr.Fatalf("Invalid regular expression for -cors-allow-origin flag: %s", err) + } + + if Flags.CorsAllowHeaders != "" { + config.AllowHeaders += ", " + Flags.CorsAllowHeaders + } + + if Flags.CorsAllowMethods != "" { + config.AllowMethods += ", " + Flags.CorsAllowMethods + } + + if Flags.CorsExposeHeaders != "" { + config.ExposeHeaders += ", " + Flags.CorsExposeHeaders + } + + return &config +} diff --git a/pkg/handler/config.go b/pkg/handler/config.go index b53b9fc75..8c5bd7a06 100644 --- a/pkg/handler/config.go +++ b/pkg/handler/config.go @@ -5,6 +5,7 @@ import ( "log" "net/url" "os" + "regexp" ) // Config provides a way to configure the Handler depending on your needs. @@ -34,7 +35,14 @@ type Config struct { DisableTermination bool // Disable cors headers. If set to true, tusd will not send any CORS related header. // This is useful if you have a proxy sitting in front of tusd that handles CORS. + // + // Deprecated: All CORS-related settings are available in via the Cors field. Use + // Cors.Disable instead of DisableCors. DisableCors bool + // Cors can be used to customize the handling of Cross-Origin Resource Sharing (CORS). + // See the CorsConfig struct for more details. + // Defaults to DefaultCorsConfig. + Cors *CorsConfig // NotifyCompleteUploads indicates whether sending notifications about // completed uploads using the CompleteUploads channel should be enabled. NotifyCompleteUploads bool @@ -64,6 +72,48 @@ type Config struct { PreFinishResponseCallback func(hook HookEvent) error } +// CorsConfig provides a way to customize the the handling of Cross-Origin Resource Sharing (CORS). +// More details about CORS are available at https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS. +type CorsConfig struct { + // Disable instructs the handler to ignore all CORS-related headers and never set a + // CORS-related header in a response. This is useful if CORS is already handled by a proxy. + Disable bool + // AllowOrigin is a regular expression used to check if a request is allowed to participate in the + // CORS protocol. If the request's Origin header matches the regular expression, CORS is allowed. + // If not, a 403 Forbidden response is sent, rejecting the CORS request. + AllowOrigin *regexp.Regexp + // AllowCredentials defines whether the `Access-Control-Allow-Credentials: true` header should be + // included in CORS responses. This allows clients to share credentials using the Cookie and + // Authorization header + AllowCredentials bool + // AllowMethods defines the value for the `Access-Control-Allow-Methods` header in the response to + // preflight requests. You can add custom methods here, but make sure that all tus-specific methods + // from DefaultConfig.AllowMethods are included as well. + AllowMethods string + // AllowHeaders defines the value for the `Access-Control-Allow-Headers` header in the response to + // preflight requests. You can add custom headers here, but make sure that all tus-specific header + // from DefaultConfig.AllowHeaders are included as well. + AllowHeaders string + // MaxAge defines the value for the `Access-Control-Max-Age` header in the response to preflight + // requests. + MaxAge string + // ExposeHeaders defines the value for the `Access-Control-Expose-Headers` header in the response to + // actual requests. You can add custom headers here, but make sure that all tus-specific header + // from DefaultConfig.ExposeHeaders are included as well. + ExposeHeaders string +} + +// DefaultCorsConfig is the configuration that will be used in none is provided. +var DefaultCorsConfig = CorsConfig{ + Disable: false, + AllowOrigin: regexp.MustCompile(".*"), + AllowCredentials: false, + AllowMethods: "POST, HEAD, PATCH, OPTIONS, GET, DELETE", + AllowHeaders: "Authorization, Origin, X-Requested-With, X-Request-ID, X-HTTP-Method-Override, Content-Type, Upload-Length, Upload-Offset, Tus-Resumable, Upload-Metadata, Upload-Defer-Length, Upload-Concat, Upload-Incomplete, Upload-Draft-Interop-Version", + MaxAge: "86400", + ExposeHeaders: "Upload-Offset, Location, Upload-Length, Tus-Version, Tus-Resumable, Tus-Max-Size, Tus-Extension, Upload-Metadata, Upload-Defer-Length, Upload-Concat, Upload-Incomplete, Upload-Draft-Interop-Version", +} + func (config *Config) validate() error { if config.Logger == nil { config.Logger = log.New(os.Stdout, "[tusd] ", log.Ldate|log.Lmicroseconds) @@ -95,5 +145,14 @@ func (config *Config) validate() error { return errors.New("tusd: StoreComposer in Config needs to contain a non-nil core") } + if config.Cors == nil { + config.Cors = &DefaultCorsConfig + } + + // Support previous settings for disabling CORS. + if config.DisableCors { + config.Cors.Disable = true + } + return nil } diff --git a/pkg/handler/cors_test.go b/pkg/handler/cors_test.go index aad2021e6..9ad2c9fa3 100644 --- a/pkg/handler/cors_test.go +++ b/pkg/handler/cors_test.go @@ -3,69 +3,238 @@ package handler_test import ( "net/http" "net/http/httptest" + "regexp" "testing" . "github.com/tus/tusd/pkg/handler" ) func TestCORS(t *testing.T) { - SubTest(t, "Preflight", func(t *testing.T, store *MockFullDataStore, composer *StoreComposer) { + SubTest(t, "DefaultConfiguration", func(t *testing.T, store *MockFullDataStore, composer *StoreComposer) { handler, _ := NewHandler(Config{ StoreComposer: composer, }) + // Preflight request (&httpTest{ Method: "OPTIONS", ReqHeader: map[string]string{ - "Origin": "tus.io", + "Origin": "https://tus.io", }, Code: http.StatusOK, ResHeader: map[string]string{ - "Access-Control-Allow-Headers": "Authorization, Origin, X-Requested-With, X-Request-ID, X-HTTP-Method-Override, Content-Type, Upload-Length, Upload-Offset, Tus-Resumable, Upload-Metadata, Upload-Defer-Length, Upload-Concat, Upload-Incomplete, Upload-Draft-Interop-Version", - "Access-Control-Allow-Methods": "POST, HEAD, PATCH, OPTIONS, GET, DELETE", - "Access-Control-Max-Age": "86400", - "Access-Control-Allow-Origin": "tus.io", + "Access-Control-Allow-Headers": "Authorization, Origin, X-Requested-With, X-Request-ID, X-HTTP-Method-Override, Content-Type, Upload-Length, Upload-Offset, Tus-Resumable, Upload-Metadata, Upload-Defer-Length, Upload-Concat, Upload-Incomplete, Upload-Draft-Interop-Version", + "Access-Control-Allow-Methods": "POST, HEAD, PATCH, OPTIONS, GET, DELETE", + "Access-Control-Max-Age": "86400", + "Access-Control-Allow-Origin": "https://tus.io", + "Vary": "Origin", + "Access-Control-Allow-Credentials": "", }, }).Run(handler, t) + + // Actual request + (&httpTest{ + Method: "POST", + ReqHeader: map[string]string{ + "Origin": "https://tus.io", + }, + ResHeader: map[string]string{ + "Access-Control-Allow-Origin": "https://tus.io", + "Access-Control-Expose-Headers": "Upload-Offset, Location, Upload-Length, Tus-Version, Tus-Resumable, Tus-Max-Size, Tus-Extension, Upload-Metadata, Upload-Defer-Length, Upload-Concat, Upload-Incomplete, Upload-Draft-Interop-Version", + "Vary": "Origin", + "Access-Control-Allow-Methods": "", + "Access-Control-Allow-Headers": "", + "Access-Control-Max-Age": "", + "Access-Control-Allow-Credentials": "", + }, + // Error response is expected + Code: http.StatusPreconditionFailed, + }).Run(handler, t) + }) + + SubTest(t, "CustomAllowedOrigin", func(t *testing.T, store *MockFullDataStore, composer *StoreComposer) { + handler, _ := NewHandler(Config{ + StoreComposer: composer, + Cors: &CorsConfig{ + AllowOrigin: regexp.MustCompile(`^https?://tus\.io$`), + AllowMethods: DefaultCorsConfig.AllowMethods, + AllowHeaders: DefaultCorsConfig.AllowHeaders, + ExposeHeaders: DefaultCorsConfig.ExposeHeaders, + MaxAge: DefaultCorsConfig.MaxAge, + }, + }) + + // Preflight request + (&httpTest{ + Method: "OPTIONS", + ReqHeader: map[string]string{ + "Origin": "http://tus.io", + }, + Code: http.StatusOK, + ResHeader: map[string]string{ + "Access-Control-Allow-Headers": "Authorization, Origin, X-Requested-With, X-Request-ID, X-HTTP-Method-Override, Content-Type, Upload-Length, Upload-Offset, Tus-Resumable, Upload-Metadata, Upload-Defer-Length, Upload-Concat, Upload-Incomplete, Upload-Draft-Interop-Version", + "Access-Control-Allow-Methods": "POST, HEAD, PATCH, OPTIONS, GET, DELETE", + "Access-Control-Max-Age": "86400", + "Access-Control-Allow-Origin": "http://tus.io", + "Vary": "Origin", + "Access-Control-Allow-Credentials": "", + }, + }).Run(handler, t) + + // Actual request + (&httpTest{ + Method: "POST", + ReqHeader: map[string]string{ + "Origin": "http://tus.io", + }, + ResHeader: map[string]string{ + "Access-Control-Allow-Origin": "http://tus.io", + "Access-Control-Expose-Headers": "Upload-Offset, Location, Upload-Length, Tus-Version, Tus-Resumable, Tus-Max-Size, Tus-Extension, Upload-Metadata, Upload-Defer-Length, Upload-Concat, Upload-Incomplete, Upload-Draft-Interop-Version", + "Vary": "Origin", + "Access-Control-Allow-Methods": "", + "Access-Control-Allow-Headers": "", + "Access-Control-Allow-Credentials": "", + "Access-Control-Max-Age": "", + }, + // Error response is expected + Code: http.StatusPreconditionFailed, + }).Run(handler, t) + }) + + SubTest(t, "CustomForbiddenOrigin", func(t *testing.T, store *MockFullDataStore, composer *StoreComposer) { + handler, _ := NewHandler(Config{ + StoreComposer: composer, + Cors: &CorsConfig{ + AllowOrigin: regexp.MustCompile(`^https?://tus\.io$`), + }, + }) + + // Preflight request + (&httpTest{ + Method: "OPTIONS", + ReqHeader: map[string]string{ + "Origin": "http://example.com", + }, + Code: http.StatusForbidden, + ResHeader: map[string]string{ + "Access-Control-Allow-Origin": "", + "Access-Control-Expose-Headers": "", + "Access-Control-Allow-Methods": "", + "Access-Control-Allow-Headers": "", + "Access-Control-Allow-Credentials": "", + "Access-Control-Max-Age": "", + }, + ResBody: "request origin is not allowed\n", + }).Run(handler, t) + + // Actual request + (&httpTest{ + Method: "POST", + ReqHeader: map[string]string{ + "Origin": "http://example.com", + }, + Code: http.StatusForbidden, + ResHeader: map[string]string{ + "Access-Control-Allow-Origin": "", + "Access-Control-Expose-Headers": "", + "Access-Control-Allow-Methods": "", + "Access-Control-Allow-Headers": "", + "Access-Control-Allow-Credentials": "", + "Access-Control-Max-Age": "", + }, + ResBody: "request origin is not allowed\n", + }).Run(handler, t) }) - SubTest(t, "Conditional allow methods", func(t *testing.T, store *MockFullDataStore, composer *StoreComposer) { + SubTest(t, "CustomConfig", func(t *testing.T, store *MockFullDataStore, composer *StoreComposer) { handler, _ := NewHandler(Config{ - StoreComposer: composer, - DisableTermination: true, - DisableDownload: true, + StoreComposer: composer, + Cors: &CorsConfig{ + AllowOrigin: regexp.MustCompile(`^https?://tus\.io$`), + AllowMethods: "POST, PATCH", + AllowHeaders: "A, B, C", + ExposeHeaders: "D, E, F", + MaxAge: "500", + AllowCredentials: true, + }, }) + // Preflight request (&httpTest{ Method: "OPTIONS", ReqHeader: map[string]string{ - "Origin": "tus.io", + "Origin": "http://tus.io", }, Code: http.StatusOK, ResHeader: map[string]string{ - "Access-Control-Allow-Headers": "Authorization, Origin, X-Requested-With, X-Request-ID, X-HTTP-Method-Override, Content-Type, Upload-Length, Upload-Offset, Tus-Resumable, Upload-Metadata, Upload-Defer-Length, Upload-Concat, Upload-Incomplete, Upload-Draft-Interop-Version", - "Access-Control-Allow-Methods": "POST, HEAD, PATCH, OPTIONS", - "Access-Control-Max-Age": "86400", - "Access-Control-Allow-Origin": "tus.io", + "Access-Control-Allow-Headers": "A, B, C", + "Access-Control-Allow-Methods": "POST, PATCH", + "Access-Control-Max-Age": "500", + "Access-Control-Allow-Origin": "http://tus.io", + "Access-Control-Allow-Credentials": "true", + "Vary": "Origin", + }, + }).Run(handler, t) + + // Actual request + (&httpTest{ + Method: "POST", + ReqHeader: map[string]string{ + "Origin": "http://tus.io", + }, + ResHeader: map[string]string{ + "Access-Control-Allow-Origin": "http://tus.io", + "Access-Control-Expose-Headers": "D, E, F", + "Access-Control-Allow-Credentials": "true", + "Vary": "Origin", + "Access-Control-Allow-Methods": "", + "Access-Control-Allow-Headers": "", + "Access-Control-Max-Age": "", }, + // Error response is expected + Code: http.StatusPreconditionFailed, }).Run(handler, t) }) - SubTest(t, "Request", func(t *testing.T, store *MockFullDataStore, composer *StoreComposer) { + SubTest(t, "DisabledConfig", func(t *testing.T, store *MockFullDataStore, composer *StoreComposer) { handler, _ := NewHandler(Config{ StoreComposer: composer, + Cors: &CorsConfig{ + Disable: true, + }, }) + // Preflight request (&httpTest{ - Name: "Actual request", - Method: "GET", + Method: "OPTIONS", + ReqHeader: map[string]string{ + "Origin": "http://example.com", + }, + Code: http.StatusOK, + ResHeader: map[string]string{ + "Access-Control-Allow-Origin": "", + "Access-Control-Expose-Headers": "", + "Access-Control-Allow-Methods": "", + "Access-Control-Allow-Headers": "", + "Access-Control-Allow-Credentials": "", + "Access-Control-Max-Age": "", + }, + }).Run(handler, t) + + // Actual request + (&httpTest{ + Method: "POST", ReqHeader: map[string]string{ - "Origin": "tus.io", + "Origin": "http://example.com", }, - Code: http.StatusMethodNotAllowed, + Code: http.StatusPreconditionFailed, ResHeader: map[string]string{ - "Access-Control-Expose-Headers": "Upload-Offset, Location, Upload-Length, Tus-Version, Tus-Resumable, Tus-Max-Size, Tus-Extension, Upload-Metadata, Upload-Defer-Length, Upload-Concat, Upload-Incomplete, Upload-Draft-Interop-Version", - "Access-Control-Allow-Origin": "tus.io", + "Access-Control-Allow-Origin": "", + "Access-Control-Expose-Headers": "", + "Access-Control-Allow-Methods": "", + "Access-Control-Allow-Headers": "", + "Access-Control-Allow-Credentials": "", + "Access-Control-Max-Age": "", }, }).Run(handler, t) }) @@ -77,7 +246,7 @@ func TestCORS(t *testing.T) { req, _ := http.NewRequest("OPTIONS", "", nil) req.Header.Set("Tus-Resumable", "1.0.0") - req.Header.Set("Origin", "tus.io") + req.Header.Set("Origin", "https://tus.io") req.Host = "tus.io" res := httptest.NewRecorder() @@ -96,20 +265,4 @@ func TestCORS(t *testing.T) { t.Errorf("expected header to contain METHOD but got: %#v", methods) } }) - - SubTest(t, "Disable CORS", func(t *testing.T, store *MockFullDataStore, composer *StoreComposer) { - handler, _ := NewHandler(Config{ - StoreComposer: composer, - DisableCors: true, - }) - - (&httpTest{ - Method: "OPTIONS", - ReqHeader: map[string]string{ - "Origin": "tus.io", - }, - Code: http.StatusOK, - ResHeader: map[string]string{}, - }).Run(handler, t) - }) } diff --git a/pkg/handler/unrouted_handler.go b/pkg/handler/unrouted_handler.go index e48757e19..0e0ab1795 100644 --- a/pkg/handler/unrouted_handler.go +++ b/pkg/handler/unrouted_handler.go @@ -72,6 +72,7 @@ var ( ErrUploadLengthAndUploadDeferLength = NewHTTPError(errors.New("provided both Upload-Length and Upload-Defer-Length"), http.StatusBadRequest) ErrInvalidUploadDeferLength = NewHTTPError(errors.New("invalid Upload-Defer-Length header"), http.StatusBadRequest) ErrUploadStoppedByServer = NewHTTPError(errors.New("upload has been stopped by server"), http.StatusBadRequest) + ErrOriginNotAllowed = NewHTTPError(errors.New("request origin is not allowed"), http.StatusForbidden) errReadTimeout = errors.New("read tcp: i/o timeout") errConnectionReset = errors.New("read tcp: connection reset by peer") @@ -213,9 +214,9 @@ func (handler *UnroutedHandler) SupportedExtensions() string { func (handler *UnroutedHandler) Middleware(h http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Allow overriding the HTTP method. The reason for this is - // that some libraries/environments to not support PATCH and - // DELETE requests, e.g. Flash in a browser and parts of Java - if newMethod := r.Header.Get("X-HTTP-Method-Override"); newMethod != "" { + // that some libraries/environments do not support PATCH and + // DELETE requests, e.g. Flash in a browser and parts of Java. + if newMethod := r.Header.Get("X-HTTP-Method-Override"); r.Method == "POST" && newMethod != "" { r.Method = newMethod } @@ -225,27 +226,29 @@ func (handler *UnroutedHandler) Middleware(h http.Handler) http.Handler { header := w.Header() - if origin := r.Header.Get("Origin"); !handler.config.DisableCors && origin != "" { - header.Set("Access-Control-Allow-Origin", origin) + cors := handler.config.Cors + if origin := r.Header.Get("Origin"); !cors.Disable && origin != "" { + originIsAllowed := cors.AllowOrigin.MatchString(origin) + if !originIsAllowed { + handler.sendError(w, r, ErrOriginNotAllowed) + return + } - if r.Method == "OPTIONS" { - allowedMethods := "POST, HEAD, PATCH, OPTIONS" - if !handler.config.DisableDownload { - allowedMethods += ", GET" - } + header.Set("Access-Control-Allow-Origin", origin) + header.Set("Vary", "Origin") - if !handler.config.DisableTermination { - allowedMethods += ", DELETE" - } + if cors.AllowCredentials { + header.Add("Access-Control-Allow-Credentials", "true") + } + if r.Method == "OPTIONS" { // Preflight request - header.Add("Access-Control-Allow-Methods", allowedMethods) - header.Add("Access-Control-Allow-Headers", "Authorization, Origin, X-Requested-With, X-Request-ID, X-HTTP-Method-Override, Content-Type, Upload-Length, Upload-Offset, Tus-Resumable, Upload-Metadata, Upload-Defer-Length, Upload-Concat, Upload-Incomplete, Upload-Draft-Interop-Version") - header.Set("Access-Control-Max-Age", "86400") - + header.Add("Access-Control-Allow-Methods", cors.AllowMethods) + header.Add("Access-Control-Allow-Headers", cors.AllowHeaders) + header.Set("Access-Control-Max-Age", cors.MaxAge) } else { // Actual request - header.Add("Access-Control-Expose-Headers", "Upload-Offset, Location, Upload-Length, Tus-Version, Tus-Resumable, Tus-Max-Size, Tus-Extension, Upload-Metadata, Upload-Defer-Length, Upload-Concat, Upload-Incomplete, Upload-Draft-Interop-Version") + header.Add("Access-Control-Expose-Headers", cors.ExposeHeaders) } }