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 }