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  }