github.com/cs3org/reva/v2@v2.27.7/internal/http/services/owncloud/ocdav/tus.go (about) 1 // Copyright 2018-2021 CERN 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 // 15 // In applying this license, CERN does not waive the privileges and immunities 16 // granted to it by virtue of its status as an Intergovernmental Organization 17 // or submit itself to any jurisdiction. 18 19 package ocdav 20 21 import ( 22 "context" 23 "encoding/json" 24 "io" 25 "net/http" 26 "path" 27 "path/filepath" 28 "strconv" 29 "strings" 30 "time" 31 32 rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" 33 link "github.com/cs3org/go-cs3apis/cs3/sharing/link/v1beta1" 34 provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" 35 typespb "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" 36 "github.com/cs3org/reva/v2/internal/http/services/owncloud/ocdav/errors" 37 "github.com/cs3org/reva/v2/internal/http/services/owncloud/ocdav/net" 38 "github.com/cs3org/reva/v2/internal/http/services/owncloud/ocdav/spacelookup" 39 "github.com/cs3org/reva/v2/pkg/appctx" 40 "github.com/cs3org/reva/v2/pkg/conversions" 41 "github.com/cs3org/reva/v2/pkg/rhttp" 42 "github.com/cs3org/reva/v2/pkg/storagespace" 43 "github.com/cs3org/reva/v2/pkg/utils" 44 "github.com/rs/zerolog" 45 tusd "github.com/tus/tusd/v2/pkg/handler" 46 "go.opentelemetry.io/otel/propagation" 47 ) 48 49 // Propagator ensures the importer module uses the same trace propagation strategy. 50 var Propagator = propagation.NewCompositeTextMapPropagator( 51 propagation.Baggage{}, 52 propagation.TraceContext{}, 53 ) 54 55 func (s *svc) handlePathTusPost(w http.ResponseWriter, r *http.Request, ns string) { 56 ctx, span := appctx.GetTracerProvider(r.Context()).Tracer(tracerName).Start(r.Context(), "tus-post") 57 defer span.End() 58 59 // read filename from metadata 60 meta := tusd.ParseMetadataHeader(r.Header.Get(net.HeaderUploadMetadata)) 61 62 // append filename to current dir 63 ref := &provider.Reference{ 64 // a path based request has no resource id, so we can only provide a path. The gateway has te figure out which provider is responsible 65 Path: path.Join(ns, r.URL.Path, meta["filename"]), 66 } 67 68 sublog := appctx.GetLogger(ctx).With().Str("path", r.URL.Path).Str("filename", meta["filename"]).Logger() 69 70 s.handleTusPost(ctx, w, r, meta, ref, sublog) 71 } 72 73 func (s *svc) handleSpacesTusPost(w http.ResponseWriter, r *http.Request, spaceID string) { 74 ctx, span := appctx.GetTracerProvider(r.Context()).Tracer(tracerName).Start(r.Context(), "spaces-tus-post") 75 defer span.End() 76 77 // read filename from metadata 78 meta := tusd.ParseMetadataHeader(r.Header.Get(net.HeaderUploadMetadata)) 79 80 ref, err := spacelookup.MakeStorageSpaceReference(spaceID, path.Join(r.URL.Path, meta["filename"])) 81 if err != nil { 82 w.WriteHeader(http.StatusBadRequest) 83 return 84 } 85 86 sublog := appctx.GetLogger(ctx).With().Str("spaceid", spaceID).Str("path", r.URL.Path).Str("filename", meta["filename"]).Logger() 87 88 s.handleTusPost(ctx, w, r, meta, &ref, sublog) 89 } 90 91 func (s *svc) handleTusPost(ctx context.Context, w http.ResponseWriter, r *http.Request, meta map[string]string, ref *provider.Reference, log zerolog.Logger) { 92 w.Header().Add(net.HeaderAccessControlAllowHeaders, strings.Join([]string{net.HeaderTusResumable, net.HeaderUploadLength, net.HeaderUploadMetadata, net.HeaderIfMatch}, ", ")) 93 w.Header().Add(net.HeaderAccessControlExposeHeaders, strings.Join([]string{net.HeaderTusResumable, net.HeaderUploadOffset, net.HeaderLocation}, ", ")) 94 w.Header().Set(net.HeaderTusExtension, "creation,creation-with-upload,checksum,expiration") 95 96 w.Header().Set(net.HeaderTusResumable, "1.0.0") 97 98 // Test if the version sent by the client is supported 99 // GET methods are not checked since a browser may visit this URL and does 100 // not include this header. This request is not part of the specification. 101 if r.Header.Get(net.HeaderTusResumable) != "1.0.0" { 102 w.WriteHeader(http.StatusPreconditionFailed) 103 return 104 } 105 if r.Header.Get(net.HeaderUploadLength) == "" { 106 w.WriteHeader(http.StatusPreconditionFailed) 107 return 108 } 109 if err := ValidateName(filename(meta["filename"]), s.nameValidators); err != nil { 110 w.WriteHeader(http.StatusPreconditionFailed) 111 return 112 } 113 114 // Test if the target is a secret filedrop 115 var isSecretFileDrop bool 116 tokenStatInfo, ok := TokenStatInfoFromContext(ctx) 117 // We assume that when the uploader can create containers, but is not allowed to list them, it is a secret file drop 118 if ok && tokenStatInfo.GetPermissionSet().CreateContainer && !tokenStatInfo.GetPermissionSet().ListContainer { 119 isSecretFileDrop = true 120 } 121 122 // r.Header.Get(net.HeaderOCChecksum) 123 // TODO must be SHA1, ADLER32 or MD5 ... in capital letters???? 124 // curl -X PUT https://demo.owncloud.com/remote.php/webdav/testcs.bin -u demo:demo -d '123' -v -H 'OC-Checksum: SHA1:40bd001563085fc35165329ea1ff5c5ecbdbbeef' 125 126 // TODO check Expect: 100-continue 127 128 client, err := s.gatewaySelector.Next() 129 if err != nil { 130 w.WriteHeader(http.StatusInternalServerError) 131 return 132 } 133 sReq := &provider.StatRequest{ 134 Ref: ref, 135 } 136 sRes, err := client.Stat(ctx, sReq) 137 if err != nil { 138 log.Error().Err(err).Msg("error sending grpc stat request") 139 w.WriteHeader(http.StatusInternalServerError) 140 return 141 } 142 143 if sRes.Status.Code != rpc.Code_CODE_OK && sRes.Status.Code != rpc.Code_CODE_NOT_FOUND { 144 errors.HandleErrorStatus(&log, w, sRes.Status) 145 return 146 } 147 148 info := sRes.Info 149 if info != nil && info.Type != provider.ResourceType_RESOURCE_TYPE_FILE { 150 log.Warn().Msg("resource is not a file") 151 w.WriteHeader(http.StatusConflict) 152 return 153 } 154 155 if info != nil { 156 clientETag := r.Header.Get(net.HeaderIfMatch) 157 serverETag := info.Etag 158 if clientETag != "" { 159 if clientETag != serverETag { 160 log.Warn().Str("client-etag", clientETag).Str("server-etag", serverETag).Msg("etags mismatch") 161 w.WriteHeader(http.StatusPreconditionFailed) 162 return 163 } 164 } 165 if isSecretFileDrop { 166 // find next filename 167 newName, status, err := FindName(ctx, client, filepath.Base(ref.Path), sRes.GetInfo().GetParentId()) 168 if err != nil { 169 log.Error().Err(err).Msg("error sending grpc stat request") 170 w.WriteHeader(http.StatusInternalServerError) 171 return 172 } 173 if status.GetCode() != rpc.Code_CODE_OK { 174 log.Error().Interface("status", status).Msg("error listing file") 175 errors.HandleErrorStatus(&log, w, status) 176 return 177 } 178 ref.Path = filepath.Join(filepath.Dir(ref.GetPath()), newName) 179 sRes.GetInfo().Name = newName 180 } 181 } 182 183 uploadLength, err := strconv.ParseInt(r.Header.Get(net.HeaderUploadLength), 10, 64) 184 if err != nil { 185 log.Debug().Err(err).Msg("wrong request") 186 w.WriteHeader(http.StatusBadRequest) 187 return 188 } 189 if uploadLength == 0 { 190 tfRes, err := client.TouchFile(ctx, &provider.TouchFileRequest{ 191 Ref: ref, 192 }) 193 if err != nil { 194 log.Error().Err(err).Msg("error sending grpc stat request") 195 w.WriteHeader(http.StatusInternalServerError) 196 return 197 } 198 switch tfRes.Status.Code { 199 case rpc.Code_CODE_OK: 200 w.Header().Set(net.HeaderLocation, "") 201 w.WriteHeader(http.StatusCreated) 202 return 203 case rpc.Code_CODE_ALREADY_EXISTS: 204 // Fall through to the tus case 205 default: 206 log.Error().Interface("status", tfRes.Status).Msg("error touching file") 207 w.WriteHeader(http.StatusInternalServerError) 208 return 209 } 210 } 211 212 opaqueMap := map[string]*typespb.OpaqueEntry{ 213 net.HeaderUploadLength: { 214 Decoder: "plain", 215 Value: []byte(r.Header.Get(net.HeaderUploadLength)), 216 }, 217 } 218 219 mtime := meta["mtime"] 220 if mtime != "" { 221 opaqueMap[net.HeaderOCMtime] = &typespb.OpaqueEntry{ 222 Decoder: "plain", 223 Value: []byte(mtime), 224 } 225 } 226 227 // initiateUpload 228 uReq := &provider.InitiateFileUploadRequest{ 229 Ref: ref, 230 Opaque: &typespb.Opaque{ 231 Map: opaqueMap, 232 }, 233 } 234 235 uRes, err := client.InitiateFileUpload(ctx, uReq) 236 if err != nil { 237 log.Error().Err(err).Msg("error initiating file upload") 238 w.WriteHeader(http.StatusInternalServerError) 239 return 240 } 241 242 if uRes.Status.Code != rpc.Code_CODE_OK { 243 if r.ProtoMajor == 1 { 244 // drain body to avoid `connection closed` errors 245 _, _ = io.Copy(io.Discard, r.Body) 246 } 247 if uRes.Status.Code == rpc.Code_CODE_NOT_FOUND { 248 w.WriteHeader(http.StatusPreconditionFailed) 249 return 250 } 251 errors.HandleErrorStatus(&log, w, uRes.Status) 252 return 253 } 254 255 var ep, token string 256 for _, p := range uRes.Protocols { 257 if p.Protocol == "tus" { 258 ep, token = p.UploadEndpoint, p.Token 259 } 260 } 261 262 // TUS clients don't understand the reva transfer token. We need to append it to the upload endpoint. 263 // The DataGateway has to take care of pulling it back into the request header upon request arrival. 264 if token != "" { 265 if !strings.HasSuffix(ep, "/") { 266 ep += "/" 267 } 268 ep += token 269 } 270 271 w.Header().Set(net.HeaderLocation, ep) 272 273 // for creation-with-upload extension forward bytes to dataprovider 274 // TODO check this really streams 275 if r.Header.Get(net.HeaderContentType) == "application/offset+octet-stream" { 276 finishUpload := true 277 if uploadLength > 0 { 278 var httpRes *http.Response 279 280 httpReq, err := rhttp.NewRequest(ctx, http.MethodPatch, ep, r.Body) 281 if err != nil { 282 log.Debug().Err(err).Msg("wrong request") 283 w.WriteHeader(http.StatusInternalServerError) 284 return 285 } 286 Propagator.Inject(ctx, propagation.HeaderCarrier(httpReq.Header)) 287 288 httpReq.Header.Set(net.HeaderContentType, r.Header.Get(net.HeaderContentType)) 289 httpReq.Header.Set(net.HeaderContentLength, r.Header.Get(net.HeaderContentLength)) 290 if r.Header.Get(net.HeaderUploadOffset) != "" { 291 httpReq.Header.Set(net.HeaderUploadOffset, r.Header.Get(net.HeaderUploadOffset)) 292 } else { 293 httpReq.Header.Set(net.HeaderUploadOffset, "0") 294 } 295 httpReq.Header.Set(net.HeaderTusResumable, r.Header.Get(net.HeaderTusResumable)) 296 297 httpRes, err = s.client.Do(httpReq) 298 if err != nil || httpRes == nil { 299 log.Error().Err(err).Msg("error doing PATCH request to data gateway") 300 w.WriteHeader(http.StatusInternalServerError) 301 return 302 } 303 defer httpRes.Body.Close() 304 305 if httpRes.StatusCode != http.StatusNoContent { 306 w.WriteHeader(httpRes.StatusCode) 307 return 308 } 309 310 w.Header().Set(net.HeaderUploadOffset, httpRes.Header.Get(net.HeaderUploadOffset)) 311 w.Header().Set(net.HeaderTusResumable, httpRes.Header.Get(net.HeaderTusResumable)) 312 w.Header().Set(net.HeaderTusUploadExpires, httpRes.Header.Get(net.HeaderTusUploadExpires)) 313 if httpRes.Header.Get(net.HeaderOCMtime) != "" { 314 w.Header().Set(net.HeaderOCMtime, httpRes.Header.Get(net.HeaderOCMtime)) 315 } 316 317 if strings.HasPrefix(uReq.GetRef().GetPath(), "/public") && uReq.GetRef().GetResourceId() == nil { 318 // Use the path based request for the public link 319 sReq.Ref.Path = uReq.Ref.GetPath() 320 sReq.Ref.ResourceId = nil 321 } else { 322 if resid, err := storagespace.ParseID(httpRes.Header.Get(net.HeaderOCFileID)); err == nil { 323 sReq.Ref = &provider.Reference{ 324 ResourceId: &resid, 325 } 326 } 327 } 328 finishUpload = httpRes.Header.Get(net.HeaderUploadOffset) == r.Header.Get(net.HeaderUploadLength) 329 } 330 331 // check if upload was fully completed 332 if uploadLength == 0 || finishUpload { 333 // get uploaded file metadata 334 335 sRes, err := client.Stat(ctx, sReq) 336 if err != nil { 337 log.Error().Err(err).Msg("error sending grpc stat request") 338 w.WriteHeader(http.StatusInternalServerError) 339 return 340 } 341 342 if sRes.Status.Code != rpc.Code_CODE_OK && sRes.Status.Code != rpc.Code_CODE_NOT_FOUND { 343 if sRes.Status.Code == rpc.Code_CODE_PERMISSION_DENIED { 344 // the token expired during upload, so the stat failed 345 // and we can't do anything about it. 346 // the clients will handle this gracefully by doing a propfind on the file 347 w.WriteHeader(http.StatusOK) 348 return 349 } 350 351 errors.HandleErrorStatus(&log, w, sRes.Status) 352 return 353 } 354 355 info := sRes.Info 356 if info == nil { 357 log.Error().Msg("No info found for uploaded file") 358 w.WriteHeader(http.StatusInternalServerError) 359 return 360 } 361 362 // get WebDav permissions for file 363 isPublic := false 364 if info.Opaque != nil && info.Opaque.Map != nil { 365 if info.Opaque.Map["link-share"] != nil && info.Opaque.Map["link-share"].Decoder == "json" { 366 ls := &link.PublicShare{} 367 _ = json.Unmarshal(info.Opaque.Map["link-share"].Value, ls) 368 isPublic = ls != nil 369 } 370 } 371 isShared := !net.IsCurrentUserOwnerOrManager(ctx, info.Owner, info) 372 role := conversions.RoleFromResourcePermissions(info.PermissionSet, isPublic) 373 permissions := role.WebDAVPermissions( 374 info.Type == provider.ResourceType_RESOURCE_TYPE_CONTAINER, 375 isShared, 376 false, 377 isPublic, 378 ) 379 380 w.Header().Set(net.HeaderContentType, info.MimeType) 381 w.Header().Set(net.HeaderOCFileID, storagespace.FormatResourceID(info.Id)) 382 w.Header().Set(net.HeaderOCETag, info.Etag) 383 w.Header().Set(net.HeaderETag, info.Etag) 384 w.Header().Set(net.HeaderOCPermissions, permissions) 385 386 t := utils.TSToTime(info.Mtime).UTC() 387 lastModifiedString := t.Format(time.RFC1123Z) 388 w.Header().Set(net.HeaderLastModified, lastModifiedString) 389 } 390 } 391 392 w.WriteHeader(http.StatusCreated) 393 }