diff --git a/changelog/unreleased/webdav-upload-return-code.md b/changelog/unreleased/webdav-upload-return-code.md new file mode 100644 index 0000000000..96754cc7c0 --- /dev/null +++ b/changelog/unreleased/webdav-upload-return-code.md @@ -0,0 +1,10 @@ +Bugfix: Fix return code for webdav uploads when the token expired + +We've fixed the behavior webdav uploads when the token expired before the final stat. +Previously clients would receive a http 500 error which is wrong, because the file +was successfully uploaded and only the stat couldn't be performed. Now we return a http 200 +ok and the clients will fetch the file info in a separate propfind request. + +Also we introduced the upload expires header on the webdav/TUS and datagateway endpoints, to signal clients how long an upload can be performed. + +https://github.com/cs3org/reva/pull/2151 diff --git a/internal/http/services/datagateway/datagateway.go b/internal/http/services/datagateway/datagateway.go index 0ba7affd55..fc866157c9 100644 --- a/internal/http/services/datagateway/datagateway.go +++ b/internal/http/services/datagateway/datagateway.go @@ -41,6 +41,8 @@ import ( const ( // TokenTransportHeader holds the header key for the reva transfer token TokenTransportHeader = "X-Reva-Transfer" + // UploadExpiresHeader holds the timestamp for the transport token expiry, defined in https://tus.io/protocols/resumable-upload.html#expiration + UploadExpiresHeader = "Upload-Expires" ) func init() { @@ -197,6 +199,9 @@ func (s *svc) doHead(w http.ResponseWriter, r *http.Request) { copyHeader(w.Header(), httpRes.Header) + // add upload expiry / transfer token expiry header for tus https://tus.io/protocols/resumable-upload.html#expiration + w.Header().Set(UploadExpiresHeader, time.Unix(claims.ExpiresAt, 0).Format(time.RFC1123)) + if httpRes.StatusCode != http.StatusOK { // swallow the body and set content-length to 0 to prevent reverse proxies from trying to read from it w.Header().Set("Content-Length", "0") diff --git a/internal/http/services/owncloud/ocdav/options.go b/internal/http/services/owncloud/ocdav/options.go index d86a5afaa1..c436eaecab 100644 --- a/internal/http/services/owncloud/ocdav/options.go +++ b/internal/http/services/owncloud/ocdav/options.go @@ -30,17 +30,17 @@ func (s *svc) handleOptions(w http.ResponseWriter, r *http.Request, ns string) { isPublic := strings.Contains(r.Context().Value(ctxKeyBaseURI).(string), "public-files") - w.Header().Set("Content-Type", "application/xml") + w.Header().Set(HeaderContentType, "application/xml") w.Header().Set("Allow", allow) w.Header().Set("DAV", "1, 2") w.Header().Set("MS-Author-Via", "DAV") if !isPublic { - w.Header().Add("Access-Control-Allow-Headers", "Tus-Resumable") - w.Header().Add("Access-Control-Expose-Headers", "Tus-Resumable, Tus-Version, Tus-Extension") - w.Header().Set("Tus-Resumable", "1.0.0") // TODO(jfd): only for dirs? - w.Header().Set("Tus-Version", "1.0.0") - w.Header().Set("Tus-Extension", "creation,creation-with-upload,checksum") - w.Header().Set("Tus-Checksum-Algorithm", "md5,sha1,crc32") + w.Header().Add(HeaderAccessControlAllowHeaders, HeaderTusResumable) + w.Header().Add(HeaderAccessControlExposeHeaders, strings.Join([]string{HeaderTusResumable, HeaderTusVersion, HeaderTusExtension}, ",")) + w.Header().Set(HeaderTusResumable, "1.0.0") // TODO(jfd): only for dirs? + w.Header().Set(HeaderTusVersion, "1.0.0") + w.Header().Set(HeaderTusExtension, "creation,creation-with-upload,checksum,expiration") + w.Header().Set(HeaderTusChecksumAlgorithm, "md5,sha1,crc32") } w.WriteHeader(http.StatusNoContent) } diff --git a/internal/http/services/owncloud/ocdav/propfind.go b/internal/http/services/owncloud/ocdav/propfind.go index e95007abc9..c94ae56e46 100644 --- a/internal/http/services/owncloud/ocdav/propfind.go +++ b/internal/http/services/owncloud/ocdav/propfind.go @@ -153,7 +153,7 @@ func (s *svc) propfindResponse(ctx context.Context, w http.ResponseWriter, r *ht w.Header().Add(HeaderAccessControlExposeHeaders, strings.Join([]string{HeaderTusResumable, HeaderTusVersion, HeaderTusExtension}, ", ")) w.Header().Set(HeaderTusResumable, "1.0.0") w.Header().Set(HeaderTusVersion, "1.0.0") - w.Header().Set(HeaderTusExtension, "creation,creation-with-upload") + w.Header().Set(HeaderTusExtension, "creation,creation-with-upload,checksum,expiration") } } w.WriteHeader(http.StatusMultiStatus) diff --git a/internal/http/services/owncloud/ocdav/tus.go b/internal/http/services/owncloud/ocdav/tus.go index 0c1eb5c4e5..bd1cf39b0e 100644 --- a/internal/http/services/owncloud/ocdav/tus.go +++ b/internal/http/services/owncloud/ocdav/tus.go @@ -29,6 +29,7 @@ import ( rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" typespb "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" + "github.com/cs3org/reva/internal/http/services/owncloud/ocs/conversions" "github.com/cs3org/reva/pkg/appctx" "github.com/cs3org/reva/pkg/rhttp" rtrace "github.com/cs3org/reva/pkg/trace" @@ -90,6 +91,7 @@ func (s *svc) handleSpacesTusPost(w http.ResponseWriter, r *http.Request, spaceI func (s *svc) handleTusPost(ctx context.Context, w http.ResponseWriter, r *http.Request, meta map[string]string, ref *provider.Reference, log zerolog.Logger) { w.Header().Add(HeaderAccessControlAllowHeaders, strings.Join([]string{HeaderTusResumable, HeaderUploadLength, HeaderUploadMetadata, HeaderIfMatch}, ", ")) w.Header().Add(HeaderAccessControlExposeHeaders, strings.Join([]string{HeaderTusResumable, HeaderLocation}, ", ")) + w.Header().Set(HeaderTusExtension, "creation,creation-with-upload,checksum,expiration") w.Header().Set(HeaderTusResumable, "1.0.0") @@ -249,6 +251,7 @@ func (s *svc) handleTusPost(ctx context.Context, w http.ResponseWriter, r *http. w.Header().Set(HeaderUploadOffset, httpRes.Header.Get(HeaderUploadOffset)) w.Header().Set(HeaderTusResumable, httpRes.Header.Get(HeaderTusResumable)) + w.Header().Set(HeaderTusUploadExpires, httpRes.Header.Get(HeaderTusUploadExpires)) if httpRes.StatusCode != http.StatusNoContent { w.WriteHeader(httpRes.StatusCode) return @@ -260,6 +263,7 @@ func (s *svc) handleTusPost(ctx context.Context, w http.ResponseWriter, r *http. // check if upload was fully completed if length == 0 || httpRes.Header.Get(HeaderUploadOffset) == r.Header.Get(HeaderUploadLength) { // get uploaded file metadata + sRes, err := client.Stat(ctx, sReq) if err != nil { log.Error().Err(err).Msg("error sending grpc stat request") @@ -268,6 +272,15 @@ func (s *svc) handleTusPost(ctx context.Context, w http.ResponseWriter, r *http. } if sRes.Status.Code != rpc.Code_CODE_OK && sRes.Status.Code != rpc.Code_CODE_NOT_FOUND { + + if sRes.Status.Code == rpc.Code_CODE_PERMISSION_DENIED { + // the token expired during upload, so the stat failed + // and we can't do anything about it. + // the clients will handle this gracefully by doing a propfind on the file + w.WriteHeader(http.StatusOK) + return + } + HandleErrorStatus(&log, w, sRes.Status) return } @@ -283,10 +296,21 @@ func (s *svc) handleTusPost(ctx context.Context, w http.ResponseWriter, r *http. w.Header().Set(HeaderOCMtime, httpRes.Header.Get(HeaderOCMtime)) } + // get WebDav permissions for file + role := conversions.RoleFromResourcePermissions(info.PermissionSet) + permissions := role.WebDAVPermissions( + info.Type == provider.ResourceType_RESOURCE_TYPE_CONTAINER, + false, + false, + false, + ) + w.Header().Set(HeaderContentType, info.MimeType) w.Header().Set(HeaderOCFileID, wrapResourceID(info.Id)) w.Header().Set(HeaderOCETag, info.Etag) w.Header().Set(HeaderETag, info.Etag) + w.Header().Set(HeaderOCPermissions, permissions) + t := utils.TSToTime(info.Mtime).UTC() lastModifiedString := t.Format(time.RFC1123Z) w.Header().Set(HeaderLastModified, lastModifiedString) diff --git a/internal/http/services/owncloud/ocdav/webdav.go b/internal/http/services/owncloud/ocdav/webdav.go index 97b61510d7..8093424795 100644 --- a/internal/http/services/owncloud/ocdav/webdav.go +++ b/internal/http/services/owncloud/ocdav/webdav.go @@ -58,11 +58,14 @@ const ( HeaderOCFileID = "OC-FileId" HeaderOCETag = "OC-ETag" HeaderOCChecksum = "OC-Checksum" + HeaderOCPermissions = "OC-Perm" HeaderDepth = "Depth" HeaderDav = "DAV" HeaderTusResumable = "Tus-Resumable" HeaderTusVersion = "Tus-Version" HeaderTusExtension = "Tus-Extension" + HeaderTusChecksumAlgorithm = "Tus-Checksum-Algorithm" + HeaderTusUploadExpires = "Upload-Expires" HeaderDestination = "Destination" HeaderOverwrite = "Overwrite" HeaderUploadChecksum = "Upload-Checksum"