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: Add flag to control CORS Origin header #942

Closed
wants to merge 4 commits into from
Closed
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
2 changes: 2 additions & 0 deletions cmd/tusd/cli/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ var Flags struct {
ShowVersion bool
ExposeMetrics bool
MetricsPath string
CorsOrigin string
BehindProxy bool
VerboseOutput bool
S3TransferAcceleration bool
Expand Down Expand Up @@ -103,6 +104,7 @@ func ParseFlags() {
flag.BoolVar(&Flags.ShowVersion, "version", false, "Print tusd version information")
flag.BoolVar(&Flags.ExposeMetrics, "expose-metrics", true, "Expose metrics about tusd usage")
flag.StringVar(&Flags.MetricsPath, "metrics-path", "/metrics", "Path under which the metrics endpoint will be accessible")
flag.StringVar(&Flags.CorsOrigin, "cors-origin", "", "Explicitly set Access-Control-Allow-Origin header")
flag.BoolVar(&Flags.BehindProxy, "behind-proxy", false, "Respect X-Forwarded-* and similar headers which may be set by proxies")
flag.BoolVar(&Flags.VerboseOutput, "verbose", true, "Enable verbose logging output")
flag.BoolVar(&Flags.S3TransferAcceleration, "s3-transfer-acceleration", false, "Use AWS S3 transfer acceleration endpoint (requires -s3-bucket option and Transfer Acceleration property on S3 bucket to be set)")
Expand Down
5 changes: 5 additions & 0 deletions cmd/tusd/cli/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ func Serve() {
config := handler.Config{
MaxSize: Flags.MaxSize,
BasePath: Flags.Basepath,
CorsOrigin: Flags.CorsOrigin,
RespectForwardedHeaders: Flags.BehindProxy,
EnableExperimentalProtocol: Flags.ExperimentalProtocol,
DisableDownload: Flags.DisableDownload,
Expand Down Expand Up @@ -106,6 +107,10 @@ func Serve() {
protocol = "https"
}

if Flags.CorsOrigin != "" {
stdout.Printf("CORS origin header is %s", Flags.CorsOrigin)
}

if Flags.HttpSock == "" {
stdout.Printf("You can now upload files to: %s://%s%s", protocol, address, basepath)
}
Expand Down
11 changes: 11 additions & 0 deletions pkg/handler/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ type Config struct {
NotifyCreatedUploads bool
// Logger is the logger to use internally, mostly for printing requests.
Logger *log.Logger
// Explicitly set Access-Control-Allow-Origin in cases where RespectForwardedHeaders
// doesn't give you the desired result. This can be the case with some reverse proxies
// or a kubernetes setup with complex network routing rules
CorsOrigin string
// Respect the X-Forwarded-Host, X-Forwarded-Proto and Forwarded headers
// potentially set by proxies when generating an absolute URL in the
// response to POST requests.
Expand Down Expand Up @@ -95,5 +99,12 @@ func (config *Config) validate() error {
return errors.New("tusd: StoreComposer in Config needs to contain a non-nil core")
}

if config.CorsOrigin != "" && config.CorsOrigin != "*" && config.CorsOrigin != "null" {
_, err := url.ParseRequestURI(config.CorsOrigin)
if err != nil {
return errors.New("tusd: CorsOrigin is not a valid URL")
}
}

return nil
}
185 changes: 149 additions & 36 deletions pkg/handler/cors_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,75 +9,204 @@ import (
)

func TestCORS(t *testing.T) {
SubTest(t, "Preflight", func(t *testing.T, store *MockFullDataStore, composer *StoreComposer) {
SubTest(t, "PreFlight - Conditional allow methods", func(t *testing.T, store *MockFullDataStore, composer *StoreComposer) {
handler, _ := NewHandler(Config{
StoreComposer: composer,
StoreComposer: composer,
CorsOrigin: "https://tus.io",
DisableTermination: true,
DisableDownload: true,
})

(&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-Allow-Methods": "POST, HEAD, PATCH, OPTIONS",
"Access-Control-Max-Age": "86400",
"Access-Control-Allow-Origin": "tus.io",
"Access-Control-Allow-Origin": "https://tus.io",
},
}).Run(handler, t)
})
SubTest(t, "PreFlight - No Origin configured", func(t *testing.T, store *MockFullDataStore, composer *StoreComposer) {
composer = NewStoreComposer()
composer.UseCore(store)

SubTest(t, "Conditional allow methods", func(t *testing.T, store *MockFullDataStore, composer *StoreComposer) {
handler, _ := NewHandler(Config{
StoreComposer: composer,
DisableTermination: true,
DisableDownload: true,
StoreComposer: composer,
CorsOrigin: "",
})

(&httpTest{
Method: "OPTIONS",
DisallowedResHeader: []string{
"Access-Control-Allow-Origin",
"Access-Control-Allow-Methods",
"Access-Control-Allow-Headers",
"Access-Control-Max-Age",
},
Code: http.StatusOK,
ReqHeader: map[string]string{
"Origin": "tus.io",
"Origin": "https://tus.io",
},
}).Run(handler, t)
})
SubTest(t, "PreFlight - Disabled CORS", func(t *testing.T, store *MockFullDataStore, composer *StoreComposer) {
composer = NewStoreComposer()
composer.UseCore(store)

handler, _ := NewHandler(Config{
StoreComposer: composer,
CorsOrigin: "",
DisableCors: true,
})

(&httpTest{
Method: "OPTIONS",
DisallowedResHeader: []string{
"Access-Control-Allow-Origin",
"Access-Control-Allow-Methods",
"Access-Control-Allow-Headers",
"Access-Control-Max-Age",
},
Code: http.StatusOK,
ReqHeader: map[string]string{
"Origin": "https://tus.io",
},
}).Run(handler, t)
})
SubTest(t, "PreFlight - Wildcard Origin", func(t *testing.T, store *MockFullDataStore, composer *StoreComposer) {
composer = NewStoreComposer()
composer.UseCore(store)

handler, _ := NewHandler(Config{
StoreComposer: composer,
CorsOrigin: "*",
})

(&httpTest{
Method: "OPTIONS",
ResHeader: map[string]string{
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "POST, HEAD, PATCH, OPTIONS, GET, DELETE",
"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-Max-Age": "86400",
},
Code: http.StatusOK,
ReqHeader: map[string]string{
"Origin": "https://tus.io",
},
}).Run(handler, t)
})
SubTest(t, "PreFlight - Matching Origin", func(t *testing.T, store *MockFullDataStore, composer *StoreComposer) {
composer = NewStoreComposer()
composer.UseCore(store)

handler, _ := NewHandler(Config{
StoreComposer: composer,
CorsOrigin: "https://tus.io",
})

(&httpTest{
Method: "OPTIONS",
ResHeader: map[string]string{
"Access-Control-Allow-Origin": "https://tus.io",
"Access-Control-Allow-Methods": "POST, HEAD, PATCH, OPTIONS, GET, DELETE",
"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",
},
Code: http.StatusOK,
ReqHeader: map[string]string{
"Origin": "https://tus.io",
},
}).Run(handler, t)
})
SubTest(t, "PreFlight - Not Matching Origin", func(t *testing.T, store *MockFullDataStore, composer *StoreComposer) {
composer = NewStoreComposer()
composer.UseCore(store)

SubTest(t, "Request", func(t *testing.T, store *MockFullDataStore, composer *StoreComposer) {
handler, _ := NewHandler(Config{
StoreComposer: composer,
CorsOrigin: "https://tus.net",
})

(&httpTest{
Name: "Actual request",
Method: "GET",
Method: "OPTIONS",
DisallowedResHeader: []string{
"Access-Control-Allow-Origin",
"Access-Control-Allow-Methods",
"Access-Control-Allow-Headers",
"Access-Control-Max-Age",
},
Code: http.StatusOK,
ReqHeader: map[string]string{
"Origin": "tus.io",
"Origin": "https://tus.io",
},
Code: http.StatusMethodNotAllowed,
}).Run(handler, t)
})
SubTest(t, "Actual Request - Wildcard Origin", func(t *testing.T, store *MockFullDataStore, composer *StoreComposer) {
composer = NewStoreComposer()
composer.UseCore(store)

handler, _ := NewHandler(Config{
StoreComposer: composer,
CorsOrigin: "*",
})

(&httpTest{
Method: "POST",
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": "Upload-Offset, Location, Upload-Length, Tus-Version, Tus-Resumable, Tus-Max-Size, Tus-Extension, Upload-Metadata, Upload-Defer-Length, Upload-Concat",
},
DisallowedResHeader: []string{
"Access-Control-Allow-Methods",
"Access-Control-Allow-Headers",
"Access-Control-Max-Age",
},
Code: http.StatusPreconditionFailed,
ReqHeader: map[string]string{
"Origin": "https://tus.io",
},
}).Run(handler, t)
})
SubTest(t, "Actual Request - Matching Origin", func(t *testing.T, store *MockFullDataStore, composer *StoreComposer) {
composer = NewStoreComposer()
composer.UseCore(store)

handler, _ := NewHandler(Config{
StoreComposer: composer,
CorsOrigin: "https://tus.io",
})

(&httpTest{
Method: "POST",
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",
},
DisallowedResHeader: []string{
"Access-Control-Allow-Methods",
"Access-Control-Allow-Headers",
"Access-Control-Max-Age",
},
Code: http.StatusPreconditionFailed,
ReqHeader: map[string]string{
"Origin": "https://tus.io",
},
}).Run(handler, t)
})
SubTest(t, "AppendHeaders", func(t *testing.T, store *MockFullDataStore, composer *StoreComposer) {
handler, _ := NewHandler(Config{
StoreComposer: composer,
})

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()
Expand All @@ -96,20 +225,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)
})
}
40 changes: 25 additions & 15 deletions pkg/handler/unrouted_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -226,23 +226,33 @@ 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)

if r.Method == "OPTIONS" {
allowedMethods := "POST, HEAD, PATCH, OPTIONS"
if !handler.config.DisableDownload {
allowedMethods += ", GET"
}

if !handler.config.DisableTermination {
allowedMethods += ", DELETE"
var configuredOrigin = handler.config.CorsOrigin
if configuredOrigin == "*" {
origin = "*"
}
if configuredOrigin == origin {
header.Set("Access-Control-Allow-Origin", origin)
header.Set("Vary", "Origin")

if r.Method == "OPTIONS" {
allowedMethods := "POST, HEAD, PATCH, OPTIONS"
if !handler.config.DisableDownload {
allowedMethods += ", GET"
}

if !handler.config.DisableTermination {
allowedMethods += ", DELETE"
}

// 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")
} 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")
}

// 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")

} 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")
Expand Down
15 changes: 12 additions & 3 deletions pkg/handler/utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,10 @@ type httpTest struct {
ReqBody io.Reader
ReqHeader map[string]string

Code int
ResBody string
ResHeader map[string]string
Code int
ResBody string
ResHeader map[string]string
DisallowedResHeader []string
}

func (test *httpTest) Run(handler http.Handler, t *testing.T) *httptest.ResponseRecorder {
Expand Down Expand Up @@ -82,6 +83,14 @@ func (test *httpTest) Run(handler http.Handler, t *testing.T) *httptest.Response
}
}

for _, key := range test.DisallowedResHeader {
header := w.Header().Get(key)

if header != "" {
t.Errorf("Not Expected '%s' (got '%s')", key, header)
}
}

if test.ResBody != "" && w.Body.String() != test.ResBody {
t.Errorf("Expected '%s' as body (got '%s'", test.ResBody, w.Body.String())
}
Expand Down