diff --git a/pkg/handler/config.go b/pkg/handler/config.go index 81660ae3..e5c67026 100644 --- a/pkg/handler/config.go +++ b/pkg/handler/config.go @@ -135,9 +135,9 @@ var DefaultCorsConfig = CorsConfig{ 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-Complete, Upload-Draft-Interop-Version", + 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-Complete, 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-Complete, Upload-Draft-Interop-Version", + 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-Complete, Upload-Draft-Interop-Version", } func (config *Config) validate() error { diff --git a/pkg/handler/cors_test.go b/pkg/handler/cors_test.go index 09744bbc..3cda591f 100644 --- a/pkg/handler/cors_test.go +++ b/pkg/handler/cors_test.go @@ -23,7 +23,7 @@ func TestCORS(t *testing.T) { }, 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-Complete, Upload-Draft-Interop-Version", + "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-Complete, 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", @@ -40,7 +40,7 @@ func TestCORS(t *testing.T) { }, 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-Complete, Upload-Draft-Interop-Version", + "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-Complete, Upload-Draft-Interop-Version", "Vary": "Origin", "Access-Control-Allow-Methods": "", "Access-Control-Allow-Headers": "", @@ -72,7 +72,7 @@ func TestCORS(t *testing.T) { }, 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-Complete, Upload-Draft-Interop-Version", + "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-Complete, 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", @@ -89,7 +89,7 @@ func TestCORS(t *testing.T) { }, 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-Complete, Upload-Draft-Interop-Version", + "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-Complete, Upload-Draft-Interop-Version", "Vary": "Origin", "Access-Control-Allow-Methods": "", "Access-Control-Allow-Headers": "", diff --git a/pkg/handler/head_test.go b/pkg/handler/head_test.go index e6c0d24a..22785a51 100644 --- a/pkg/handler/head_test.go +++ b/pkg/handler/head_test.go @@ -145,105 +145,106 @@ func TestHead(t *testing.T) { }) SubTest(t, "ExperimentalProtocol", func(t *testing.T, _ *MockFullDataStore, _ *StoreComposer) { - SubTest(t, "IncompleteUpload", func(t *testing.T, store *MockFullDataStore, composer *StoreComposer) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - upload := NewMockFullUpload(ctrl) - - gomock.InOrder( - store.EXPECT().GetUpload(gomock.Any(), "yes").Return(upload, nil), - upload.EXPECT().GetInfo(gomock.Any()).Return(FileInfo{ - SizeIsDeferred: false, - Size: 10, - Offset: 5, - }, nil), - ) - - handler, _ := NewHandler(Config{ - StoreComposer: composer, - EnableExperimentalProtocol: true, + for _, interopVersion := range []string{"3", "4"} { + SubTest(t, "InteropVersion"+interopVersion, func(t *testing.T, _ *MockFullDataStore, _ *StoreComposer) { + SubTest(t, "IncompleteUpload", func(t *testing.T, store *MockFullDataStore, composer *StoreComposer) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + upload := NewMockFullUpload(ctrl) + + gomock.InOrder( + store.EXPECT().GetUpload(gomock.Any(), "yes").Return(upload, nil), + upload.EXPECT().GetInfo(gomock.Any()).Return(FileInfo{ + SizeIsDeferred: false, + Size: 10, + Offset: 5, + }, nil), + ) + + handler, _ := NewHandler(Config{ + StoreComposer: composer, + EnableExperimentalProtocol: true, + }) + + (&httpTest{ + Method: "HEAD", + URL: "yes", + ReqHeader: map[string]string{ + "Upload-Draft-Interop-Version": interopVersion, + }, + Code: http.StatusNoContent, + ResHeader: addIETFUploadCompleteHeader(map[string]string{ + "Upload-Draft-Interop-Version": interopVersion, + "Upload-Offset": "5", + }, false, interopVersion), + }).Run(handler, t) + }) + + SubTest(t, "CompleteUpload", func(t *testing.T, store *MockFullDataStore, composer *StoreComposer) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + upload := NewMockFullUpload(ctrl) + + gomock.InOrder( + store.EXPECT().GetUpload(gomock.Any(), "yes").Return(upload, nil), + upload.EXPECT().GetInfo(gomock.Any()).Return(FileInfo{ + SizeIsDeferred: false, + Size: 10, + Offset: 10, + }, nil), + ) + + handler, _ := NewHandler(Config{ + StoreComposer: composer, + EnableExperimentalProtocol: true, + }) + + (&httpTest{ + Method: "HEAD", + URL: "yes", + ReqHeader: map[string]string{ + "Upload-Draft-Interop-Version": interopVersion, + }, + Code: http.StatusNoContent, + ResHeader: addIETFUploadCompleteHeader(map[string]string{ + "Upload-Draft-Interop-Version": interopVersion, + "Upload-Offset": "10", + }, true, interopVersion), + }).Run(handler, t) + }) + + SubTest(t, "DeferredLength", func(t *testing.T, store *MockFullDataStore, composer *StoreComposer) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + upload := NewMockFullUpload(ctrl) + + gomock.InOrder( + store.EXPECT().GetUpload(gomock.Any(), "yes").Return(upload, nil), + upload.EXPECT().GetInfo(gomock.Any()).Return(FileInfo{ + SizeIsDeferred: true, + Offset: 5, + }, nil), + ) + + handler, _ := NewHandler(Config{ + StoreComposer: composer, + EnableExperimentalProtocol: true, + }) + + (&httpTest{ + Method: "HEAD", + URL: "yes", + ReqHeader: map[string]string{ + "Upload-Draft-Interop-Version": interopVersion, + }, + Code: http.StatusNoContent, + ResHeader: addIETFUploadCompleteHeader(map[string]string{ + "Upload-Draft-Interop-Version": interopVersion, + "Upload-Offset": "5", + }, false, interopVersion), + }).Run(handler, t) + }) }) - - (&httpTest{ - Method: "HEAD", - URL: "yes", - ReqHeader: map[string]string{ - "Upload-Draft-Interop-Version": "4", - }, - Code: http.StatusNoContent, - ResHeader: map[string]string{ - "Upload-Draft-Interop-Version": "4", - "Upload-Complete": "?0", - "Upload-Offset": "5", - }, - }).Run(handler, t) - }) - - SubTest(t, "CompleteUpload", func(t *testing.T, store *MockFullDataStore, composer *StoreComposer) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - upload := NewMockFullUpload(ctrl) - - gomock.InOrder( - store.EXPECT().GetUpload(gomock.Any(), "yes").Return(upload, nil), - upload.EXPECT().GetInfo(gomock.Any()).Return(FileInfo{ - SizeIsDeferred: false, - Size: 10, - Offset: 10, - }, nil), - ) - - handler, _ := NewHandler(Config{ - StoreComposer: composer, - EnableExperimentalProtocol: true, - }) - - (&httpTest{ - Method: "HEAD", - URL: "yes", - ReqHeader: map[string]string{ - "Upload-Draft-Interop-Version": "4", - }, - Code: http.StatusNoContent, - ResHeader: map[string]string{ - "Upload-Draft-Interop-Version": "4", - "Upload-Complete": "?1", - "Upload-Offset": "10", - }, - }).Run(handler, t) - }) - - SubTest(t, "DeferredLength", func(t *testing.T, store *MockFullDataStore, composer *StoreComposer) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - upload := NewMockFullUpload(ctrl) - - gomock.InOrder( - store.EXPECT().GetUpload(gomock.Any(), "yes").Return(upload, nil), - upload.EXPECT().GetInfo(gomock.Any()).Return(FileInfo{ - SizeIsDeferred: true, - Offset: 5, - }, nil), - ) - - handler, _ := NewHandler(Config{ - StoreComposer: composer, - EnableExperimentalProtocol: true, - }) - - (&httpTest{ - Method: "HEAD", - URL: "yes", - ReqHeader: map[string]string{ - "Upload-Draft-Interop-Version": "4", - }, - Code: http.StatusNoContent, - ResHeader: map[string]string{ - "Upload-Draft-Interop-Version": "4", - "Upload-Complete": "?0", - "Upload-Offset": "5", - }, - }).Run(handler, t) - }) + } }) } diff --git a/pkg/handler/patch_test.go b/pkg/handler/patch_test.go index 36e8b2c9..29999018 100644 --- a/pkg/handler/patch_test.go +++ b/pkg/handler/patch_test.go @@ -811,159 +811,159 @@ func TestPatch(t *testing.T) { }) SubTest(t, "ExperimentalProtocol", func(t *testing.T, _ *MockFullDataStore, _ *StoreComposer) { - SubTest(t, "CompleteUploadWithKnownSize", func(t *testing.T, store *MockFullDataStore, composer *StoreComposer) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - upload := NewMockFullUpload(ctrl) - - gomock.InOrder( - store.EXPECT().GetUpload(gomock.Any(), "yes").Return(upload, nil), - upload.EXPECT().GetInfo(gomock.Any()).Return(FileInfo{ - ID: "yes", - Offset: 5, - Size: 10, - SizeIsDeferred: false, - }, nil), - upload.EXPECT().WriteChunk(gomock.Any(), int64(5), NewReaderMatcher("hello")).Return(int64(5), nil), - upload.EXPECT().FinishUpload(gomock.Any()), - ) - - handler, _ := NewHandler(Config{ - StoreComposer: composer, - EnableExperimentalProtocol: true, + for _, interopVersion := range []string{"3", "4"} { + SubTest(t, "InteropVersion"+interopVersion, func(t *testing.T, _ *MockFullDataStore, _ *StoreComposer) { + SubTest(t, "CompleteUploadWithKnownSize", func(t *testing.T, store *MockFullDataStore, composer *StoreComposer) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + upload := NewMockFullUpload(ctrl) + + gomock.InOrder( + store.EXPECT().GetUpload(gomock.Any(), "yes").Return(upload, nil), + upload.EXPECT().GetInfo(gomock.Any()).Return(FileInfo{ + ID: "yes", + Offset: 5, + Size: 10, + SizeIsDeferred: false, + }, nil), + upload.EXPECT().WriteChunk(gomock.Any(), int64(5), NewReaderMatcher("hello")).Return(int64(5), nil), + upload.EXPECT().FinishUpload(gomock.Any()), + ) + + handler, _ := NewHandler(Config{ + StoreComposer: composer, + EnableExperimentalProtocol: true, + }) + + (&httpTest{ + Method: "PATCH", + URL: "yes", + ReqHeader: addIETFUploadCompleteHeader(map[string]string{ + "Upload-Draft-Interop-Version": interopVersion, + "Upload-Offset": "5", + }, true, interopVersion), + ReqBody: strings.NewReader("hello"), + Code: http.StatusNoContent, + ResHeader: map[string]string{ + "Upload-Offset": "10", + }, + }).Run(handler, t) + }) + SubTest(t, "CompleteUploadWithUnknownSize", func(t *testing.T, store *MockFullDataStore, composer *StoreComposer) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + upload := NewMockFullUpload(ctrl) + + gomock.InOrder( + store.EXPECT().GetUpload(gomock.Any(), "yes").Return(upload, nil), + upload.EXPECT().GetInfo(gomock.Any()).Return(FileInfo{ + ID: "yes", + Offset: 5, + Size: 0, + SizeIsDeferred: true, + }, nil), + upload.EXPECT().WriteChunk(gomock.Any(), int64(5), NewReaderMatcher("hello")).Return(int64(5), nil), + upload.EXPECT().GetInfo(gomock.Any()).Return(FileInfo{ + ID: "yes", + Offset: 10, + Size: 0, + SizeIsDeferred: true, + }, nil), + store.EXPECT().AsLengthDeclarableUpload(upload).Return(upload), + upload.EXPECT().DeclareLength(gomock.Any(), int64(10)), + upload.EXPECT().FinishUpload(gomock.Any()), + ) + + handler, _ := NewHandler(Config{ + StoreComposer: composer, + EnableExperimentalProtocol: true, + }) + + (&httpTest{ + Method: "PATCH", + URL: "yes", + ReqHeader: addIETFUploadCompleteHeader(map[string]string{ + "Upload-Draft-Interop-Version": interopVersion, + "Upload-Offset": "5", + }, true, interopVersion), + ReqBody: strings.NewReader("hello"), + Code: http.StatusNoContent, + ResHeader: map[string]string{ + "Upload-Offset": "10", + }, + }).Run(handler, t) + }) + SubTest(t, "ContinueUploadWithKnownSize", func(t *testing.T, store *MockFullDataStore, composer *StoreComposer) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + upload := NewMockFullUpload(ctrl) + + gomock.InOrder( + store.EXPECT().GetUpload(gomock.Any(), "yes").Return(upload, nil), + upload.EXPECT().GetInfo(gomock.Any()).Return(FileInfo{ + ID: "yes", + Offset: 5, + Size: 10, + SizeIsDeferred: false, + }, nil), + upload.EXPECT().WriteChunk(gomock.Any(), int64(5), NewReaderMatcher("hel")).Return(int64(3), nil), + ) + + handler, _ := NewHandler(Config{ + StoreComposer: composer, + EnableExperimentalProtocol: true, + }) + + (&httpTest{ + Method: "PATCH", + URL: "yes", + ReqHeader: addIETFUploadCompleteHeader(map[string]string{ + "Upload-Draft-Interop-Version": interopVersion, + "Upload-Offset": "5", + }, false, interopVersion), + ReqBody: strings.NewReader("hel"), + Code: http.StatusNoContent, + ResHeader: map[string]string{ + "Upload-Offset": "8", + }, + }).Run(handler, t) + }) + SubTest(t, "ContinueUploadWithUnknownSize", func(t *testing.T, store *MockFullDataStore, composer *StoreComposer) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + upload := NewMockFullUpload(ctrl) + + gomock.InOrder( + store.EXPECT().GetUpload(gomock.Any(), "yes").Return(upload, nil), + upload.EXPECT().GetInfo(gomock.Any()).Return(FileInfo{ + ID: "yes", + Offset: 5, + Size: 0, + SizeIsDeferred: true, + }, nil), + upload.EXPECT().WriteChunk(gomock.Any(), int64(5), NewReaderMatcher("hel")).Return(int64(3), nil), + ) + + handler, _ := NewHandler(Config{ + StoreComposer: composer, + EnableExperimentalProtocol: true, + }) + + (&httpTest{ + Method: "PATCH", + URL: "yes", + ReqHeader: addIETFUploadCompleteHeader(map[string]string{ + "Upload-Draft-Interop-Version": interopVersion, + "Upload-Offset": "5", + }, false, interopVersion), + ReqBody: strings.NewReader("hel"), + Code: http.StatusNoContent, + ResHeader: map[string]string{ + "Upload-Offset": "8", + }, + }).Run(handler, t) + }) }) - - (&httpTest{ - Method: "PATCH", - URL: "yes", - ReqHeader: map[string]string{ - "Upload-Draft-Interop-Version": "4", - "Upload-Offset": "5", - "Upload-Complete": "?1", - }, - ReqBody: strings.NewReader("hello"), - Code: http.StatusNoContent, - ResHeader: map[string]string{ - "Upload-Offset": "10", - }, - }).Run(handler, t) - }) - SubTest(t, "CompleteUploadWithUnknownSize", func(t *testing.T, store *MockFullDataStore, composer *StoreComposer) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - upload := NewMockFullUpload(ctrl) - - gomock.InOrder( - store.EXPECT().GetUpload(gomock.Any(), "yes").Return(upload, nil), - upload.EXPECT().GetInfo(gomock.Any()).Return(FileInfo{ - ID: "yes", - Offset: 5, - Size: 0, - SizeIsDeferred: true, - }, nil), - upload.EXPECT().WriteChunk(gomock.Any(), int64(5), NewReaderMatcher("hello")).Return(int64(5), nil), - upload.EXPECT().GetInfo(gomock.Any()).Return(FileInfo{ - ID: "yes", - Offset: 10, - Size: 0, - SizeIsDeferred: true, - }, nil), - store.EXPECT().AsLengthDeclarableUpload(upload).Return(upload), - upload.EXPECT().DeclareLength(gomock.Any(), int64(10)), - upload.EXPECT().FinishUpload(gomock.Any()), - ) - - handler, _ := NewHandler(Config{ - StoreComposer: composer, - EnableExperimentalProtocol: true, - }) - - (&httpTest{ - Method: "PATCH", - URL: "yes", - ReqHeader: map[string]string{ - "Upload-Draft-Interop-Version": "4", - "Upload-Offset": "5", - "Upload-Complete": "?1", - }, - ReqBody: strings.NewReader("hello"), - Code: http.StatusNoContent, - ResHeader: map[string]string{ - "Upload-Offset": "10", - }, - }).Run(handler, t) - }) - SubTest(t, "ContinueUploadWithKnownSize", func(t *testing.T, store *MockFullDataStore, composer *StoreComposer) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - upload := NewMockFullUpload(ctrl) - - gomock.InOrder( - store.EXPECT().GetUpload(gomock.Any(), "yes").Return(upload, nil), - upload.EXPECT().GetInfo(gomock.Any()).Return(FileInfo{ - ID: "yes", - Offset: 5, - Size: 10, - SizeIsDeferred: false, - }, nil), - upload.EXPECT().WriteChunk(gomock.Any(), int64(5), NewReaderMatcher("hel")).Return(int64(3), nil), - ) - - handler, _ := NewHandler(Config{ - StoreComposer: composer, - EnableExperimentalProtocol: true, - }) - - (&httpTest{ - Method: "PATCH", - URL: "yes", - ReqHeader: map[string]string{ - "Upload-Draft-Interop-Version": "4", - "Upload-Offset": "5", - "Upload-Complete": "?0", - }, - ReqBody: strings.NewReader("hel"), - Code: http.StatusNoContent, - ResHeader: map[string]string{ - "Upload-Offset": "8", - }, - }).Run(handler, t) - }) - SubTest(t, "ContinueUploadWithUnknownSize", func(t *testing.T, store *MockFullDataStore, composer *StoreComposer) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - upload := NewMockFullUpload(ctrl) - - gomock.InOrder( - store.EXPECT().GetUpload(gomock.Any(), "yes").Return(upload, nil), - upload.EXPECT().GetInfo(gomock.Any()).Return(FileInfo{ - ID: "yes", - Offset: 5, - Size: 0, - SizeIsDeferred: true, - }, nil), - upload.EXPECT().WriteChunk(gomock.Any(), int64(5), NewReaderMatcher("hel")).Return(int64(3), nil), - ) - - handler, _ := NewHandler(Config{ - StoreComposer: composer, - EnableExperimentalProtocol: true, - }) - - (&httpTest{ - Method: "PATCH", - URL: "yes", - ReqHeader: map[string]string{ - "Upload-Draft-Interop-Version": "4", - "Upload-Offset": "5", - "Upload-Complete": "?0", - }, - ReqBody: strings.NewReader("hel"), - Code: http.StatusNoContent, - ResHeader: map[string]string{ - "Upload-Offset": "8", - }, - }).Run(handler, t) - }) + } }) } diff --git a/pkg/handler/post_test.go b/pkg/handler/post_test.go index 96b97c11..8a0e1256 100644 --- a/pkg/handler/post_test.go +++ b/pkg/handler/post_test.go @@ -546,137 +546,139 @@ func TestPost(t *testing.T) { }) SubTest(t, "ExperimentalProtocol", func(t *testing.T, _ *MockFullDataStore, _ *StoreComposer) { - SubTest(t, "CompleteUpload", func(t *testing.T, store *MockFullDataStore, _ *StoreComposer) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - locker := NewMockFullLocker(ctrl) - lock := NewMockFullLock(ctrl) - upload := NewMockFullUpload(ctrl) - - gomock.InOrder( - store.EXPECT().NewUpload(gomock.Any(), FileInfo{ - SizeIsDeferred: false, - Size: 11, - MetaData: map[string]string{ - "filename": "hello.txt", - "filetype": "text/plain", - }, - }).Return(upload, nil), - upload.EXPECT().GetInfo(gomock.Any()).Return(FileInfo{ - ID: "foo", - SizeIsDeferred: false, - Size: 11, - MetaData: map[string]string{ - "filename": "hello.txt", - "filetype": "text/plain", - }, - }, nil), - locker.EXPECT().NewLock("foo").Return(lock, nil), - lock.EXPECT().Lock(gomock.Any(), gomock.Any()).Return(nil), - upload.EXPECT().WriteChunk(gomock.Any(), int64(0), NewReaderMatcher("hello world")).Return(int64(11), nil), - upload.EXPECT().FinishUpload(gomock.Any()).Return(nil), - lock.EXPECT().Unlock().Return(nil), - ) - - composer := NewStoreComposer() - composer.UseCore(store) - composer.UseLocker(locker) - - handler, _ := NewHandler(Config{ - StoreComposer: composer, - BasePath: "/files/", - EnableExperimentalProtocol: true, - }) - - res := (&httpTest{ - Method: "POST", - ReqHeader: map[string]string{ - "Upload-Draft-Interop-Version": "4", - "Upload-Complete": "?1", - "Content-Type": "text/plain; charset=utf-8", - "Content-Disposition": "attachment; filename=hello.txt", - }, - ReqBody: strings.NewReader("hello world"), - Code: http.StatusCreated, - ResHeader: map[string]string{ - "Upload-Draft-Interop-Version": "4", - "Location": "http://tus.io/files/foo", - "Upload-Offset": "11", - }, - }).Run(handler, t) - - a := assert.New(t) - a.Equal([]httptestrecorder.InformationalResponse{ - { - Code: 104, - Header: http.Header{ - "Upload-Draft-Interop-Version": []string{"4"}, - "Location": []string{"http://tus.io/files/foo"}, - "X-Content-Type-Options": []string{"nosniff"}, - }, - }, - }, res.InformationalResponses) - }) - - SubTest(t, "IncompleteUpload", func(t *testing.T, store *MockFullDataStore, _ *StoreComposer) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - locker := NewMockFullLocker(ctrl) - lock := NewMockFullLock(ctrl) - upload := NewMockFullUpload(ctrl) - - gomock.InOrder( - store.EXPECT().NewUpload(gomock.Any(), FileInfo{ - SizeIsDeferred: true, - MetaData: map[string]string{}, - }).Return(upload, nil), - upload.EXPECT().GetInfo(gomock.Any()).Return(FileInfo{ - ID: "foo", - SizeIsDeferred: true, - }, nil), - locker.EXPECT().NewLock("foo").Return(lock, nil), - lock.EXPECT().Lock(gomock.Any(), gomock.Any()).Return(nil), - upload.EXPECT().WriteChunk(gomock.Any(), int64(0), NewReaderMatcher("hello world")).Return(int64(11), nil), - lock.EXPECT().Unlock().Return(nil), - ) - - composer := NewStoreComposer() - composer.UseCore(store) - composer.UseLocker(locker) - composer.UseLengthDeferrer(store) - - handler, _ := NewHandler(Config{ - StoreComposer: composer, - BasePath: "/files/", - EnableExperimentalProtocol: true, + for _, interopVersion := range []string{"3", "4"} { + SubTest(t, "InteropVersion"+interopVersion, func(t *testing.T, _ *MockFullDataStore, _ *StoreComposer) { + SubTest(t, "CompleteUpload", func(t *testing.T, store *MockFullDataStore, _ *StoreComposer) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + locker := NewMockFullLocker(ctrl) + lock := NewMockFullLock(ctrl) + upload := NewMockFullUpload(ctrl) + + gomock.InOrder( + store.EXPECT().NewUpload(gomock.Any(), FileInfo{ + SizeIsDeferred: false, + Size: 11, + MetaData: map[string]string{ + "filename": "hello.txt", + "filetype": "text/plain", + }, + }).Return(upload, nil), + upload.EXPECT().GetInfo(gomock.Any()).Return(FileInfo{ + ID: "foo", + SizeIsDeferred: false, + Size: 11, + MetaData: map[string]string{ + "filename": "hello.txt", + "filetype": "text/plain", + }, + }, nil), + locker.EXPECT().NewLock("foo").Return(lock, nil), + lock.EXPECT().Lock(gomock.Any(), gomock.Any()).Return(nil), + upload.EXPECT().WriteChunk(gomock.Any(), int64(0), NewReaderMatcher("hello world")).Return(int64(11), nil), + upload.EXPECT().FinishUpload(gomock.Any()).Return(nil), + lock.EXPECT().Unlock().Return(nil), + ) + + composer := NewStoreComposer() + composer.UseCore(store) + composer.UseLocker(locker) + + handler, _ := NewHandler(Config{ + StoreComposer: composer, + BasePath: "/files/", + EnableExperimentalProtocol: true, + }) + + res := (&httpTest{ + Method: "POST", + ReqHeader: addIETFUploadCompleteHeader(map[string]string{ + "Upload-Draft-Interop-Version": interopVersion, + "Content-Type": "text/plain; charset=utf-8", + "Content-Disposition": "attachment; filename=hello.txt", + }, true, interopVersion), + ReqBody: strings.NewReader("hello world"), + Code: http.StatusCreated, + ResHeader: map[string]string{ + "Upload-Draft-Interop-Version": interopVersion, + "Location": "http://tus.io/files/foo", + "Upload-Offset": "11", + }, + }).Run(handler, t) + + a := assert.New(t) + a.Equal([]httptestrecorder.InformationalResponse{ + { + Code: 104, + Header: http.Header{ + "Upload-Draft-Interop-Version": []string{interopVersion}, + "Location": []string{"http://tus.io/files/foo"}, + "X-Content-Type-Options": []string{"nosniff"}, + }, + }, + }, res.InformationalResponses) + }) + + SubTest(t, "IncompleteUpload", func(t *testing.T, store *MockFullDataStore, _ *StoreComposer) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + locker := NewMockFullLocker(ctrl) + lock := NewMockFullLock(ctrl) + upload := NewMockFullUpload(ctrl) + + gomock.InOrder( + store.EXPECT().NewUpload(gomock.Any(), FileInfo{ + SizeIsDeferred: true, + MetaData: map[string]string{}, + }).Return(upload, nil), + upload.EXPECT().GetInfo(gomock.Any()).Return(FileInfo{ + ID: "foo", + SizeIsDeferred: true, + }, nil), + locker.EXPECT().NewLock("foo").Return(lock, nil), + lock.EXPECT().Lock(gomock.Any(), gomock.Any()).Return(nil), + upload.EXPECT().WriteChunk(gomock.Any(), int64(0), NewReaderMatcher("hello world")).Return(int64(11), nil), + lock.EXPECT().Unlock().Return(nil), + ) + + composer := NewStoreComposer() + composer.UseCore(store) + composer.UseLocker(locker) + composer.UseLengthDeferrer(store) + + handler, _ := NewHandler(Config{ + StoreComposer: composer, + BasePath: "/files/", + EnableExperimentalProtocol: true, + }) + + res := (&httpTest{ + Method: "POST", + ReqHeader: addIETFUploadCompleteHeader(map[string]string{ + "Upload-Draft-Interop-Version": interopVersion, + }, false, interopVersion), + ReqBody: strings.NewReader("hello world"), + Code: http.StatusCreated, + ResHeader: map[string]string{ + "Upload-Draft-Interop-Version": interopVersion, + "Location": "http://tus.io/files/foo", + "Upload-Offset": "11", + }, + }).Run(handler, t) + + a := assert.New(t) + a.Equal([]httptestrecorder.InformationalResponse{ + { + Code: 104, + Header: http.Header{ + "Upload-Draft-Interop-Version": []string{interopVersion}, + "Location": []string{"http://tus.io/files/foo"}, + "X-Content-Type-Options": []string{"nosniff"}, + }, + }, + }, res.InformationalResponses) + }) }) - - res := (&httpTest{ - Method: "POST", - ReqHeader: map[string]string{ - "Upload-Draft-Interop-Version": "4", - "Upload-Complete": "?0", - }, - ReqBody: strings.NewReader("hello world"), - Code: http.StatusCreated, - ResHeader: map[string]string{ - "Upload-Draft-Interop-Version": "4", - "Location": "http://tus.io/files/foo", - "Upload-Offset": "11", - }, - }).Run(handler, t) - - a := assert.New(t) - a.Equal([]httptestrecorder.InformationalResponse{ - { - Code: 104, - Header: http.Header{ - "Upload-Draft-Interop-Version": []string{"4"}, - "Location": []string{"http://tus.io/files/foo"}, - "X-Content-Type-Options": []string{"nosniff"}, - }, - }, - }, res.InformationalResponses) - }) + } }) } diff --git a/pkg/handler/unrouted_handler.go b/pkg/handler/unrouted_handler.go index 14b471d9..50283f8e 100644 --- a/pkg/handler/unrouted_handler.go +++ b/pkg/handler/unrouted_handler.go @@ -18,7 +18,16 @@ import ( ) const UploadLengthDeferred = "1" -const currentUploadDraftInteropVersion = "4" + +type draftVersion string + +// These are the different interoperability versions defines in the different +// versions of the resumable uploads draft from the HTTP working group. +// See https://datatracker.ietf.org/doc/draft-ietf-httpbis-resumable-upload/ +const ( + interopVersion3 draftVersion = "3" // From draft version -01 + interopVersion4 draftVersion = "4" // From draft version -02 +) var ( reForwardedHost = regexp.MustCompile(`host="?([^;"]+)`) @@ -211,7 +220,7 @@ func (handler *UnroutedHandler) Middleware(h http.Handler) http.Handler { } // Detect requests with tus v1 protocol vs the IETF resumable upload draft - isTusV1 := !handler.isResumableUploadDraftRequest(r) + isTusV1 := !handler.usesIETFDraft(r) if isTusV1 { // Set current version used by the server @@ -260,7 +269,7 @@ func (handler *UnroutedHandler) Middleware(h http.Handler) http.Handler { // PostFile creates a new file upload using the datastore after validating the // length and parsing the metadata. func (handler *UnroutedHandler) PostFile(w http.ResponseWriter, r *http.Request) { - if handler.isResumableUploadDraftRequest(r) { + if handler.usesIETFDraft(r) { handler.PostFileV2(w, r) return } @@ -438,17 +447,18 @@ func (handler *UnroutedHandler) PostFile(w http.ResponseWriter, r *http.Request) // PostFile creates a new file upload using the datastore after validating the // length and parsing the metadata. func (handler *UnroutedHandler) PostFileV2(w http.ResponseWriter, r *http.Request) { + currentUploadDraftInteropVersion := getIETFDraftInteropVersion(r) c := handler.getContext(w, r) // Parse headers contentType := r.Header.Get("Content-Type") contentDisposition := r.Header.Get("Content-Disposition") - isComplete := r.Header.Get("Upload-Complete") == "?1" + willCompleteUpload := isIETFDraftUploadComplete(r) info := FileInfo{ MetaData: make(MetaData), } - if isComplete && r.ContentLength != -1 { + if willCompleteUpload && r.ContentLength != -1 { // If the client wants to perform the upload in one request with Content-Length, we know the final upload size. info.Size = r.ContentLength } else { @@ -535,7 +545,7 @@ func (handler *UnroutedHandler) PostFileV2(w http.ResponseWriter, r *http.Reques // Send 104 response w.Header().Set("Location", url) - w.Header().Set("Upload-Draft-Interop-Version", currentUploadDraftInteropVersion) + w.Header().Set("Upload-Draft-Interop-Version", string(currentUploadDraftInteropVersion)) w.WriteHeader(104) handler.Metrics.incUploadsCreated() @@ -565,7 +575,7 @@ func (handler *UnroutedHandler) PostFileV2(w http.ResponseWriter, r *http.Reques } // 4. Finish upload, if necessary - if isComplete && info.SizeIsDeferred { + if willCompleteUpload && info.SizeIsDeferred { info, err = upload.GetInfo(c) if err != nil { handler.sendError(c, err) @@ -634,7 +644,7 @@ func (handler *UnroutedHandler) HeadFile(w http.ResponseWriter, r *http.Request) }, } - if !handler.isResumableUploadDraftRequest(r) { + if !handler.usesIETFDraft(r) { // Add Upload-Concat header if possible if info.IsPartial { resp.Header["Upload-Concat"] = "partial" @@ -664,14 +674,9 @@ func (handler *UnroutedHandler) HeadFile(w http.ResponseWriter, r *http.Request) resp.StatusCode = http.StatusOK } else { - if !info.SizeIsDeferred && info.Offset == info.Size { - // Upload is complete if we know the size and it matches the offset. - resp.Header["Upload-Complete"] = "?1" - } else { - resp.Header["Upload-Complete"] = "?0" - } - - resp.Header["Upload-Draft-Interop-Version"] = currentUploadDraftInteropVersion + isUploadCompleteNow := !info.SizeIsDeferred && info.Offset == info.Size + setIETFDraftUploadComplete(r, resp, isUploadCompleteNow) + resp.Header["Upload-Draft-Interop-Version"] = string(getIETFDraftInteropVersion(r)) // Draft requires a 204 No Content response resp.StatusCode = http.StatusNoContent @@ -685,7 +690,7 @@ func (handler *UnroutedHandler) HeadFile(w http.ResponseWriter, r *http.Request) func (handler *UnroutedHandler) PatchFile(w http.ResponseWriter, r *http.Request) { c := handler.getContext(w, r) - isTusV1 := !handler.isResumableUploadDraftRequest(r) + isTusV1 := !handler.usesIETFDraft(r) // Check for presence of application/offset+octet-stream if isTusV1 && r.Header.Get("Content-Type") != "application/offset+octet-stream" { @@ -740,7 +745,7 @@ func (handler *UnroutedHandler) PatchFile(w http.ResponseWriter, r *http.Request return } - // TODO: If Upload-Complete: ?1 and Content-Length is set, we can + // TODO: If (Upload-Incomplete: ?0 OR Upload-Complete: ?1) and (Content-Length is set), we can // - declare the length already here // - validate that the length from this request matches info.Size if !info.SizeIsDeferred @@ -787,8 +792,8 @@ func (handler *UnroutedHandler) PatchFile(w http.ResponseWriter, r *http.Request return } - isComplete := r.Header.Get("Upload-Complete") == "?1" - if isComplete && info.SizeIsDeferred { + willCompleteUpload := isIETFDraftUploadComplete(r) + if willCompleteUpload && info.SizeIsDeferred { info, err = upload.GetInfo(c) if err != nil { handler.sendError(c, err) @@ -1348,10 +1353,57 @@ func (handler *UnroutedHandler) lockUpload(c *httpContext, id string) (Lock, err return lock, nil } -// isResumableUploadDraftRequest returns whether a HTTP request includes a sign that it is -// related to resumable upload draft from IETF (instead of tus v1) -func (handler UnroutedHandler) isResumableUploadDraftRequest(r *http.Request) bool { - return handler.config.EnableExperimentalProtocol && r.Header.Get("Upload-Draft-Interop-Version") == currentUploadDraftInteropVersion +// usesIETFDraft returns whether a HTTP request uses a supported version of the resumable upload draft from IETF +// (instead of tus v1) and support has been enabled in tusd. +func (handler UnroutedHandler) usesIETFDraft(r *http.Request) bool { + interopVersionHeader := getIETFDraftInteropVersion(r) + return handler.config.EnableExperimentalProtocol && interopVersionHeader != "" +} + +// getIETFDraftInteropVersion returns the resumable upload draft interop version from the headers. +func getIETFDraftInteropVersion(r *http.Request) draftVersion { + version := draftVersion(r.Header.Get("Upload-Draft-Interop-Version")) + switch version { + case interopVersion3, interopVersion4: + return version + default: + return "" + } +} + +// isIETFDraftUploadComplete returns whether a HTTP request upload is complete +// according to the set resumable upload draft version from IETF. +func isIETFDraftUploadComplete(r *http.Request) bool { + currentUploadDraftInteropVersion := getIETFDraftInteropVersion(r) + switch currentUploadDraftInteropVersion { + case interopVersion4: + return r.Header.Get("Upload-Complete") == "?1" + case interopVersion3: + return r.Header.Get("Upload-Incomplete") == "?0" + default: + return false + } +} + +// setIETFDraftUploadComplete sets the Upload-Complete (Upload-Incomplete) to the provided +// value, depending on the interop version used in the request. +func setIETFDraftUploadComplete(r *http.Request, resp HTTPResponse, isComplete bool) { + currentUploadDraftInteropVersion := getIETFDraftInteropVersion(r) + + switch currentUploadDraftInteropVersion { + case interopVersion3: + if isComplete { + resp.Header["Upload-Incomplete"] = "?0" + } else { + resp.Header["Upload-Incomplete"] = "?1" + } + case interopVersion4: + if isComplete { + resp.Header["Upload-Complete"] = "?1" + } else { + resp.Header["Upload-Complete"] = "?0" + } + } } // ParseMetadataHeader parses the Upload-Metadata header as defined in the diff --git a/pkg/handler/utils_test.go b/pkg/handler/utils_test.go index 40858cdb..f8463899 100644 --- a/pkg/handler/utils_test.go +++ b/pkg/handler/utils_test.go @@ -132,3 +132,23 @@ func (m readerMatcher) Matches(x interface{}) bool { func (m readerMatcher) String() string { return fmt.Sprintf("reads to %s", m.expect) } + +// addIETFUploadCompleteHeader writes the Upload-Complete or Upload-Incomplete header depending on the interop version. +func addIETFUploadCompleteHeader(header map[string]string, isComplete bool, interopVersion string) map[string]string { + switch interopVersion { + case "3": + if isComplete { + header["Upload-Incomplete"] = "?0" + } else { + header["Upload-Incomplete"] = "?1" + } + case "4": + if isComplete { + header["Upload-Complete"] = "?1" + } else { + header["Upload-Complete"] = "?0" + } + } + + return header +}