github.com/cs3org/reva/v2@v2.27.7/internal/http/services/owncloud/ocdav/put.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  	"io"
    24  	"net/http"
    25  	"path"
    26  	"path/filepath"
    27  	"strconv"
    28  	"strings"
    29  
    30  	rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
    31  	provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
    32  	typespb "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
    33  	"github.com/cs3org/reva/v2/internal/http/services/datagateway"
    34  	"github.com/cs3org/reva/v2/internal/http/services/owncloud/ocdav/errors"
    35  	"github.com/cs3org/reva/v2/internal/http/services/owncloud/ocdav/net"
    36  	"github.com/cs3org/reva/v2/internal/http/services/owncloud/ocdav/spacelookup"
    37  	"github.com/cs3org/reva/v2/pkg/appctx"
    38  	"github.com/cs3org/reva/v2/pkg/errtypes"
    39  	"github.com/cs3org/reva/v2/pkg/rhttp"
    40  	"github.com/cs3org/reva/v2/pkg/storagespace"
    41  	"github.com/cs3org/reva/v2/pkg/utils"
    42  	"github.com/rs/zerolog"
    43  	"go.opentelemetry.io/otel/propagation"
    44  )
    45  
    46  func sufferMacOSFinder(r *http.Request) bool {
    47  	return r.Header.Get(net.HeaderExpectedEntityLength) != ""
    48  }
    49  
    50  func handleMacOSFinder(w http.ResponseWriter, r *http.Request) error {
    51  	/*
    52  	   Many webservers will not cooperate well with Finder PUT requests,
    53  	   because it uses 'Chunked' transfer encoding for the request body.
    54  	   The symptom of this problem is that Finder sends files to the
    55  	   server, but they arrive as 0-length files.
    56  	   If we don't do anything, the user might think they are uploading
    57  	   files successfully, but they end up empty on the server. Instead,
    58  	   we throw back an error if we detect this.
    59  	   The reason Finder uses Chunked, is because it thinks the files
    60  	   might change as it's being uploaded, and therefore the
    61  	   Content-Length can vary.
    62  	   Instead it sends the X-Expected-Entity-Length header with the size
    63  	   of the file at the very start of the request. If this header is set,
    64  	   but we don't get a request body we will fail the request to
    65  	   protect the end-user.
    66  	*/
    67  
    68  	log := appctx.GetLogger(r.Context())
    69  	content := r.Header.Get(net.HeaderContentLength)
    70  	expected := r.Header.Get(net.HeaderExpectedEntityLength)
    71  	log.Warn().Str("content-length", content).Str("x-expected-entity-length", expected).Msg("Mac OS Finder corner-case detected")
    72  
    73  	// The best mitigation to this problem is to tell users to not use crappy Finder.
    74  	// Another possible mitigation is to change the use the value of X-Expected-Entity-Length header in the Content-Length header.
    75  	expectedInt, err := strconv.ParseInt(expected, 10, 64)
    76  	if err != nil {
    77  		log.Error().Err(err).Msg("error parsing expected length")
    78  		w.WriteHeader(http.StatusBadRequest)
    79  		return err
    80  	}
    81  	r.ContentLength = expectedInt
    82  	return nil
    83  }
    84  
    85  func isContentRange(r *http.Request) bool {
    86  	/*
    87  		   Content-Range is dangerous for PUT requests:  PUT per definition
    88  		   stores a full resource.  draft-ietf-httpbis-p2-semantics-15 says
    89  		   in section 7.6:
    90  			 An origin server SHOULD reject any PUT request that contains a
    91  			 Content-Range header field, since it might be misinterpreted as
    92  			 partial content (or might be partial content that is being mistakenly
    93  			 PUT as a full representation).  Partial content updates are possible
    94  			 by targeting a separately identified resource with state that
    95  			 overlaps a portion of the larger resource, or by using a different
    96  			 method that has been specifically defined for partial updates (for
    97  			 example, the PATCH method defined in [RFC5789]).
    98  		   This clarifies RFC2616 section 9.6:
    99  			 The recipient of the entity MUST NOT ignore any Content-*
   100  			 (e.g. Content-Range) headers that it does not understand or implement
   101  			 and MUST return a 501 (Not Implemented) response in such cases.
   102  		   OTOH is a PUT request with a Content-Range currently the only way to
   103  		   continue an aborted upload request and is supported by curl, mod_dav,
   104  		   Tomcat and others.  Since some clients do use this feature which results
   105  		   in unexpected behaviour (cf PEAR::HTTP_WebDAV_Client 1.0.1), we reject
   106  		   all PUT requests with a Content-Range for now.
   107  	*/
   108  	return r.Header.Get(net.HeaderContentRange) != ""
   109  }
   110  
   111  func (s *svc) handlePathPut(w http.ResponseWriter, r *http.Request, ns string) {
   112  	ctx, span := appctx.GetTracerProvider(r.Context()).Tracer(tracerName).Start(r.Context(), "put")
   113  	defer span.End()
   114  
   115  	fn := path.Join(ns, r.URL.Path)
   116  	sublog := appctx.GetLogger(ctx).With().Str("path", fn).Logger()
   117  
   118  	if err := ValidateName(filename(r.URL.Path), s.nameValidators); err != nil {
   119  		w.WriteHeader(http.StatusBadRequest)
   120  		b, err := errors.Marshal(http.StatusBadRequest, err.Error(), "", "")
   121  		errors.HandleWebdavError(&sublog, w, b, err)
   122  		return
   123  	}
   124  
   125  	space, status, err := spacelookup.LookUpStorageSpaceForPath(ctx, s.gatewaySelector, fn)
   126  	if err != nil {
   127  		sublog.Error().Err(err).Str("path", fn).Msg("failed to look up storage space")
   128  		w.WriteHeader(http.StatusInternalServerError)
   129  		return
   130  	}
   131  	if status.Code != rpc.Code_CODE_OK {
   132  		errors.HandleErrorStatus(&sublog, w, status)
   133  		return
   134  	}
   135  
   136  	s.handlePut(ctx, w, r, spacelookup.MakeRelativeReference(space, fn, false), sublog)
   137  }
   138  
   139  func (s *svc) handlePut(ctx context.Context, w http.ResponseWriter, r *http.Request, ref *provider.Reference, log zerolog.Logger) {
   140  	if !checkPreconditions(w, r, log) {
   141  		// checkPreconditions handles error returns
   142  		return
   143  	}
   144  
   145  	length, err := getContentLength(r)
   146  	if err != nil {
   147  		log.Error().Err(err).Msg("error getting the content length")
   148  		w.WriteHeader(http.StatusBadRequest)
   149  		return
   150  	}
   151  
   152  	client, err := s.gatewaySelector.Next()
   153  	if err != nil {
   154  		log.Error().Err(err).Msg("error selecting next gateway client")
   155  		w.WriteHeader(http.StatusInternalServerError)
   156  		return
   157  	}
   158  
   159  	// Test if the target is a secret filedrop
   160  	tokenStatInfo, ok := TokenStatInfoFromContext(ctx)
   161  	// We assume that when the uploader can create containers, but is not allowed to list them, it is a secret file drop
   162  	if ok && tokenStatInfo.GetPermissionSet().CreateContainer && !tokenStatInfo.GetPermissionSet().ListContainer {
   163  		// TODO we can skip this stat if the tokenStatInfo is the direct parent
   164  		sReq := &provider.StatRequest{
   165  			Ref: ref,
   166  		}
   167  		sRes, err := client.Stat(ctx, sReq)
   168  		if err != nil {
   169  			log.Error().Err(err).Msg("error sending grpc stat request")
   170  			w.WriteHeader(http.StatusInternalServerError)
   171  			return
   172  		}
   173  
   174  		// We also need to continue if we are not allowed to stat a resource. We may not have stat permission. That still means it exists and we need to find a new filename.
   175  		switch sRes.Status.Code {
   176  		case rpc.Code_CODE_OK, rpc.Code_CODE_PERMISSION_DENIED:
   177  			// find next filename
   178  			newName, status, err := FindName(ctx, client, filepath.Base(ref.Path), sRes.GetInfo().GetParentId())
   179  			if err != nil {
   180  				log.Error().Err(err).Msg("error sending grpc stat request")
   181  				w.WriteHeader(http.StatusInternalServerError)
   182  				return
   183  			}
   184  			if status.Code != rpc.Code_CODE_OK {
   185  				log.Error().Interface("status", status).Msg("error listing file")
   186  				errors.HandleErrorStatus(&log, w, status)
   187  				return
   188  			}
   189  			ref.Path = utils.MakeRelativePath(filepath.Join(filepath.Dir(ref.GetPath()), newName))
   190  		case rpc.Code_CODE_NOT_FOUND:
   191  			// just continue with normal upload
   192  		default:
   193  			log.Error().Interface("status", sRes.Status).Msg("error stating file")
   194  			errors.HandleErrorStatus(&log, w, sRes.Status)
   195  			return
   196  		}
   197  	}
   198  
   199  	opaque := &typespb.Opaque{}
   200  	if mtime := r.Header.Get(net.HeaderOCMtime); mtime != "" {
   201  		utils.AppendPlainToOpaque(opaque, net.HeaderOCMtime, mtime)
   202  
   203  		// TODO: find a way to check if the storage really accepted the value
   204  		w.Header().Set(net.HeaderOCMtime, "accepted")
   205  	}
   206  	if length == 0 {
   207  		tfRes, err := client.TouchFile(ctx, &provider.TouchFileRequest{
   208  			Opaque: opaque,
   209  			Ref:    ref,
   210  		})
   211  		if err != nil {
   212  			log.Error().Err(err).Msg("error sending grpc touch file request")
   213  			w.WriteHeader(http.StatusInternalServerError)
   214  			return
   215  		}
   216  		if tfRes.Status.Code == rpc.Code_CODE_OK {
   217  			sRes, err := client.Stat(ctx, &provider.StatRequest{
   218  				Ref: ref,
   219  			})
   220  			if err != nil {
   221  				log.Error().Err(err).Msg("error sending grpc touch file request")
   222  				w.WriteHeader(http.StatusInternalServerError)
   223  				return
   224  			}
   225  			if sRes.Status.Code != rpc.Code_CODE_OK {
   226  				log.Error().Interface("status", sRes.Status).Msg("error touching file")
   227  				errors.HandleErrorStatus(&log, w, sRes.Status)
   228  				return
   229  			}
   230  
   231  			w.Header().Set(net.HeaderETag, sRes.Info.Etag)
   232  			w.Header().Set(net.HeaderOCETag, sRes.Info.Etag)
   233  			w.Header().Set(net.HeaderOCFileID, storagespace.FormatResourceID(sRes.Info.Id))
   234  			w.Header().Set(net.HeaderLastModified, net.RFC1123Z(sRes.Info.Mtime))
   235  
   236  			w.WriteHeader(http.StatusCreated)
   237  			return
   238  		}
   239  
   240  		if tfRes.Status.Code != rpc.Code_CODE_ALREADY_EXISTS {
   241  			log.Error().Interface("status", tfRes.Status).Msg("error touching file")
   242  			errors.HandleErrorStatus(&log, w, tfRes.Status)
   243  			return
   244  		}
   245  	}
   246  
   247  	utils.AppendPlainToOpaque(opaque, net.HeaderUploadLength, strconv.FormatInt(length, 10))
   248  
   249  	// curl -X PUT https://demo.owncloud.com/remote.php/webdav/testcs.bin -u demo:demo -d '123' -v -H 'OC-Checksum: SHA1:40bd001563085fc35165329ea1ff5c5ecbdbbeef'
   250  
   251  	var cparts []string
   252  	// TUS Upload-Checksum header takes precedence
   253  	if checksum := r.Header.Get(net.HeaderUploadChecksum); checksum != "" {
   254  		cparts = strings.SplitN(checksum, " ", 2)
   255  		if len(cparts) != 2 {
   256  			log.Debug().Str("upload-checksum", checksum).Msg("invalid Upload-Checksum format, expected '[algorithm] [checksum]'")
   257  			w.WriteHeader(http.StatusBadRequest)
   258  			return
   259  		}
   260  		// Then try owncloud header
   261  	} else if checksum := r.Header.Get(net.HeaderOCChecksum); checksum != "" {
   262  		cparts = strings.SplitN(checksum, ":", 2)
   263  		if len(cparts) != 2 {
   264  			log.Debug().Str("oc-checksum", checksum).Msg("invalid OC-Checksum format, expected '[algorithm]:[checksum]'")
   265  			w.WriteHeader(http.StatusBadRequest)
   266  			return
   267  		}
   268  	}
   269  	// we do not check the algorithm here, because it might depend on the storage
   270  	if len(cparts) == 2 {
   271  		// Translate into TUS style Upload-Checksum header
   272  		// algorithm is always lowercase, checksum is separated by space
   273  		utils.AppendPlainToOpaque(opaque, net.HeaderUploadChecksum, strings.ToLower(cparts[0])+" "+cparts[1])
   274  	}
   275  
   276  	uReq := &provider.InitiateFileUploadRequest{
   277  		Ref:    ref,
   278  		Opaque: opaque,
   279  		LockId: requestLockToken(r),
   280  	}
   281  	if ifMatch := r.Header.Get(net.HeaderIfMatch); ifMatch != "" {
   282  		uReq.Options = &provider.InitiateFileUploadRequest_IfMatch{IfMatch: ifMatch}
   283  	}
   284  
   285  	// where to upload the file?
   286  	uRes, err := client.InitiateFileUpload(ctx, uReq)
   287  	if err != nil {
   288  		log.Error().Err(err).Msg("error initiating file upload")
   289  		w.WriteHeader(http.StatusInternalServerError)
   290  		return
   291  	}
   292  
   293  	if uRes.Status.Code != rpc.Code_CODE_OK {
   294  		if r.ProtoMajor == 1 {
   295  			// drain body to avoid `connection closed` errors
   296  			_, _ = io.Copy(io.Discard, r.Body)
   297  		}
   298  		switch uRes.Status.Code {
   299  		case rpc.Code_CODE_PERMISSION_DENIED:
   300  			status := http.StatusForbidden
   301  			m := uRes.Status.Message
   302  			// check if user has access to parent
   303  			sRes, err := client.Stat(ctx, &provider.StatRequest{Ref: &provider.Reference{
   304  				ResourceId: ref.ResourceId,
   305  				Path:       utils.MakeRelativePath(path.Dir(ref.Path)),
   306  			}})
   307  			if err != nil {
   308  				log.Error().Err(err).Msg("error performing stat grpc request")
   309  				w.WriteHeader(http.StatusInternalServerError)
   310  				return
   311  			}
   312  			if sRes.Status.Code != rpc.Code_CODE_OK {
   313  				// return not found error so we do not leak existence of a file
   314  				// TODO hide permission failed for users without access in every kind of request
   315  				// TODO should this be done in the driver?
   316  				status = http.StatusNotFound
   317  			}
   318  			if status == http.StatusNotFound {
   319  				m = "Resource not found" // mimic the oc10 error message
   320  			}
   321  			w.WriteHeader(status)
   322  			b, err := errors.Marshal(status, m, "", "")
   323  			errors.HandleWebdavError(&log, w, b, err)
   324  		case rpc.Code_CODE_ABORTED:
   325  			w.WriteHeader(http.StatusPreconditionFailed)
   326  		case rpc.Code_CODE_FAILED_PRECONDITION:
   327  			w.WriteHeader(http.StatusConflict)
   328  		default:
   329  			errors.HandleErrorStatus(&log, w, uRes.Status)
   330  		}
   331  		return
   332  	}
   333  
   334  	// ony send actual PUT request if file has bytes. Otherwise the initiate file upload request creates the file
   335  	if length != 0 {
   336  		var ep, token string
   337  		for _, p := range uRes.Protocols {
   338  			if p.Protocol == "simple" {
   339  				ep, token = p.UploadEndpoint, p.Token
   340  			}
   341  		}
   342  
   343  		httpReq, err := rhttp.NewRequest(ctx, http.MethodPut, ep, r.Body)
   344  		if err != nil {
   345  			w.WriteHeader(http.StatusInternalServerError)
   346  			return
   347  		}
   348  		Propagator.Inject(ctx, propagation.HeaderCarrier(httpReq.Header))
   349  		httpReq.Header.Set(datagateway.TokenTransportHeader, token)
   350  		httpReq.ContentLength = length
   351  
   352  		httpRes, err := s.client.Do(httpReq)
   353  		if err != nil {
   354  			log.Error().Err(err).Msg("error doing PUT request to data service")
   355  			w.WriteHeader(http.StatusInternalServerError)
   356  			return
   357  		}
   358  		defer httpRes.Body.Close()
   359  		if httpRes.StatusCode != http.StatusOK {
   360  			if httpRes.StatusCode == http.StatusPartialContent {
   361  				w.WriteHeader(http.StatusPartialContent)
   362  				return
   363  			}
   364  			if httpRes.StatusCode == errtypes.StatusChecksumMismatch {
   365  				w.WriteHeader(http.StatusBadRequest)
   366  				b, err := errors.Marshal(http.StatusBadRequest, "The computed checksum does not match the one received from the client.", "", "")
   367  				errors.HandleWebdavError(&log, w, b, err)
   368  				return
   369  			}
   370  			log.Error().Err(err).Msg("PUT request to data server failed")
   371  			w.WriteHeader(httpRes.StatusCode)
   372  			return
   373  		}
   374  
   375  		// copy headers if they are present
   376  		if httpRes.Header.Get(net.HeaderETag) != "" {
   377  			w.Header().Set(net.HeaderETag, httpRes.Header.Get(net.HeaderETag))
   378  		}
   379  		if httpRes.Header.Get(net.HeaderOCETag) != "" {
   380  			w.Header().Set(net.HeaderOCETag, httpRes.Header.Get(net.HeaderOCETag))
   381  		}
   382  		if httpRes.Header.Get(net.HeaderOCFileID) != "" {
   383  			w.Header().Set(net.HeaderOCFileID, httpRes.Header.Get(net.HeaderOCFileID))
   384  		}
   385  		if httpRes.Header.Get(net.HeaderLastModified) != "" {
   386  			w.Header().Set(net.HeaderLastModified, httpRes.Header.Get(net.HeaderLastModified))
   387  		}
   388  	}
   389  
   390  	// file was new
   391  	// FIXME make created flag a property on the InitiateFileUploadResponse
   392  	if created := utils.ReadPlainFromOpaque(uRes.Opaque, "created"); created == "true" {
   393  		w.WriteHeader(http.StatusCreated)
   394  		return
   395  	}
   396  
   397  	// overwrite
   398  	w.WriteHeader(http.StatusNoContent)
   399  }
   400  
   401  func (s *svc) handleSpacesPut(w http.ResponseWriter, r *http.Request, spaceID string) {
   402  	ctx, span := appctx.GetTracerProvider(r.Context()).Tracer(tracerName).Start(r.Context(), "spaces_put")
   403  	defer span.End()
   404  
   405  	sublog := appctx.GetLogger(ctx).With().Str("spaceid", spaceID).Str("path", r.URL.Path).Logger()
   406  
   407  	ref, err := spacelookup.MakeStorageSpaceReference(spaceID, r.URL.Path)
   408  	if err != nil {
   409  		w.WriteHeader(http.StatusBadRequest)
   410  		return
   411  	}
   412  
   413  	if ref.GetResourceId().GetOpaqueId() != "" && ref.GetResourceId().GetSpaceId() != ref.GetResourceId().GetOpaqueId() && r.URL.Path == "/" {
   414  		s.handlePut(ctx, w, r, &ref, sublog)
   415  		return
   416  	}
   417  
   418  	if err := ValidateName(filename(ref.Path), s.nameValidators); err != nil {
   419  		w.WriteHeader(http.StatusBadRequest)
   420  		b, err := errors.Marshal(http.StatusBadRequest, err.Error(), "", "")
   421  		errors.HandleWebdavError(&sublog, w, b, err)
   422  		return
   423  	}
   424  
   425  	s.handlePut(ctx, w, r, &ref, sublog)
   426  }
   427  
   428  func checkPreconditions(w http.ResponseWriter, r *http.Request, log zerolog.Logger) bool {
   429  	if isContentRange(r) {
   430  		log.Debug().Msg("Content-Range not supported for PUT")
   431  		w.WriteHeader(http.StatusNotImplemented)
   432  		return false
   433  	}
   434  
   435  	if sufferMacOSFinder(r) {
   436  		err := handleMacOSFinder(w, r)
   437  		if err != nil {
   438  			log.Debug().Err(err).Msg("error handling Mac OS corner-case")
   439  			w.WriteHeader(http.StatusInternalServerError)
   440  			return false
   441  		}
   442  	}
   443  	return true
   444  }
   445  
   446  func getContentLength(r *http.Request) (int64, error) {
   447  	length, err := strconv.ParseInt(r.Header.Get(net.HeaderContentLength), 10, 64)
   448  	if err != nil {
   449  		// Fallback to Upload-Length
   450  		length, err = strconv.ParseInt(r.Header.Get(net.HeaderUploadLength), 10, 64)
   451  		if err != nil {
   452  			return 0, err
   453  		}
   454  	}
   455  	return length, nil
   456  }