From eb44e83a993efc2380408a8ea7bfc6d86c6957d3 Mon Sep 17 00:00:00 2001 From: David Christofas Date: Thu, 15 Jul 2021 16:19:26 +0200 Subject: [PATCH] Refactor dav (#1888) --- changelog/unreleased/refactor-dav.md | 6 + .../http/services/owncloud/ocdav/avatars.go | 6 +- internal/http/services/owncloud/ocdav/copy.go | 316 +++++++++++------- .../http/services/owncloud/ocdav/delete.go | 31 +- internal/http/services/owncloud/ocdav/get.go | 90 +++-- internal/http/services/owncloud/ocdav/head.go | 32 +- .../http/services/owncloud/ocdav/mkcol.go | 33 +- internal/http/services/owncloud/ocdav/move.go | 100 +++--- .../http/services/owncloud/ocdav/ocdav.go | 12 +- .../http/services/owncloud/ocdav/propfind.go | 140 ++++---- .../http/services/owncloud/ocdav/proppatch.go | 102 +++--- .../services/owncloud/ocdav/publicfile.go | 20 +- internal/http/services/owncloud/ocdav/put.go | 150 +++++---- .../http/services/owncloud/ocdav/trashbin.go | 27 +- internal/http/services/owncloud/ocdav/tus.go | 125 +++---- .../http/services/owncloud/ocdav/versions.go | 8 +- .../http/services/owncloud/ocdav/webdav.go | 86 ++++- 17 files changed, 738 insertions(+), 546 deletions(-) create mode 100644 changelog/unreleased/refactor-dav.md diff --git a/changelog/unreleased/refactor-dav.md b/changelog/unreleased/refactor-dav.md new file mode 100644 index 0000000000..2d7b45cd1e --- /dev/null +++ b/changelog/unreleased/refactor-dav.md @@ -0,0 +1,6 @@ +Enhancement: Refactoring of the webdav code + +Refactored the webdav code to make it reusable. + +https://github.com/cs3org/reva/pull/1888 + diff --git a/internal/http/services/owncloud/ocdav/avatars.go b/internal/http/services/owncloud/ocdav/avatars.go index 441abbca38..b133ff4b8d 100644 --- a/internal/http/services/owncloud/ocdav/avatars.go +++ b/internal/http/services/owncloud/ocdav/avatars.go @@ -40,7 +40,7 @@ func (h *AvatarsHandler) Handler(s *svc) http.Handler { ctx := r.Context() log := appctx.GetLogger(ctx) - if r.Method == "OPTIONS" { + if r.Method == http.MethodOptions { // no need for the user, and we need to be able // to answer preflight checks, which have no auth headers r.URL.Path = "/" // always use / ... we just want the options answered so phoenix doesnt hiccup @@ -49,7 +49,7 @@ func (h *AvatarsHandler) Handler(s *svc) http.Handler { } _, r.URL.Path = router.ShiftPath(r.URL.Path) - if r.Method == "GET" && r.URL.Path == "/128.png" { + if r.Method == http.MethodGet && r.URL.Path == "/128.png" { // TODO load avatar url from user context? const img = "89504E470D0A1A0A0000000D4948445200000080000000800806000000C33E61CB00000006624B474400FF00FF00FFA0BDA793000000097048597300000B1300000B1301009A9C180000000774494D4507E3061B080516D3ECF61E000008F24944415478DAED9D7D8C1D5515C07FDB76774B775BB7454AA54BBB2D5DDD765B6BD34A140B464CB07EA0113518016B4848FC438A1A448D9A18FF40316942524D544C900F49A17C2882120A28604D506C915AB160B7B2A14275B7BB606BCB76779F7FDC79F4CE79F7BD7DEFED7B3377E69E5FF2B233DBED9B7B3EEECCB977CE3D171445511445511445098B9680645D0BAC8C7EAE020A0E5D0C027B80DDC033EA1ED96521B001D80A3C1F19BB9ECF007003F0CEE83B15CFB90C781A189986D1CB7D8E007F06AE5035FBC599C0359181AA35E6716014188A3EA3D1EFAAFDFFAF025F06DEA2EA4F97EB81935318EB047037F0396035300FE8043A8039D1A723FADD3CA01FB80AB817989CE2BB4F0237AA1992E703C00B150CB313D812057DD36555D4DB7756B8DE41E0236A9664B8A982216E897A72B3980BDC5CE1CE70AB9AA779744541984BF1DF03BA136C4B77F4F871B5E519E074355763590E8C9519A62D4DB15DDDC07E47BBC681156AB6C6D0071C7328F93A60A607ED9B017CDED1BEA35140A94C83259122ED67EE316093876DDD28E61F26A3B69EAD66AC9F61D1AB463D1F7BCF075E126D1E5233D6C74EC7E4CBEA0CB47B317048B4FD6135676D5C2E14F83A705686DA3FD771F7D229E41A823E19507D2A83729CEF90A34FCD3B35F70BA5DD906159AE14B2FC5ACD5B99F384C20E016D19966726B04FC874819AB93C434259EFCD814C2B1C2319C5C14542513FCF916C5B856C17ABB94BF915F1A9D43CCDA2AD20FEDAFA5135779CD9A287FC2D8732EE12322E52B39FE28742391B722863BF90F17635BBA115386C296630C7B2DA492CFFC16423A5CA0C0F94B214938A55E4DE9CC73945E691EEAB6C6F1C605D140314F96D8E1DE009EBB82D923D78EE14CFC63C67DA9E2D64DDA1E687D7882751E49D717452E80DE692DFC99F723C26646E0F390638579C3F1280033CEE888182758035E27C57000EF09438EF0BD9017AC5F940000EB0479CF784EC004BACE362E66FDE1916E7DD213BC07CEBF8BF8104BE72B4B330640768B58E8F0734FA39661D7785EA002DE2FA2703790448676F0DD901EC123593013D02267CB90BCF48591105E110A13051A12304E500E3BEDC0A136666858E105410683B407B20778116605699BB41700E30621DCF09E80E709A757C22640778D93A9E1B501C603BFB70C80EF092753C0B3FD6FB27815DC6E65F213B80CCFFEB0DC0F8B27CCC3F43768003E27C6D000E20339E5F08D9019E9B423979E43C71BE97C0B1B3639E0A40DE3F089983E72FC4EBEAE41DBBDED1F36937C687B4703B55BA050F72E59B488F18EA3EAE0E509A07B826C70E2083DC87D5014C143C669DAFCFB103D8B28D3B82E020E9225EEA3DCF2B839EB4E41C414BCABEC19E4022635BC67D3E346886278AF99138BF3487C6DF2CCE7FA2FD3EEE8876EF78368732CA6251AD6AF6D2D180BDA54B9E6AEC2E25BE25CD633EF53C5FD86E1DCF06DE9D2307D8487C09FC1DDADF4B5981C98E29F692277224DB1F2DB926D0BD04CAF2AC784E2ECB814CB236D05E3573792E10CABA270732FD46C874A19AB9320396B286C9F664C9424C1188A23C2FFA38FCF20D3B185C80D9222EAB7C0C7893757EA7F6EFA9E9A174E3C7AC22B797D3E0AF4AEE168AFB520665F8AA90E101356BF57489DEB39F6C958D6FA77467D337AB59ABA705784828F033196AFF15A2ED8F12D6DAC786B086D22D57B2B07A688EA3DDEBD59CF5F103A1C86D1968F336D1E69FAA19EB6701A6744C5666079789B61ED367FFF4F99650EA11FC5C42D64A3CB3A9007C57CDD7189E168AFDBE876DBC91FCE734A4463F66F3485BC11FF4A87D978AB68D11C632B744B99AD2DD44CFF1A05DEB89BFC62E00D7AAB99AC30EA1E8D7800E8F82BE02709F9AA9799C46E9DE820748A7E2F65B8997BA2F06A81D6AA6E6D289C9A9B7153F98F070EB3D8E9E3F4AFCCD9FD244563B0C3044325BB17DC271ED02F02E354BF2C1D70987219AB9A6E0DA32C6FFA49A231DFACA18647B13AE7553996B6D5333A4CB324CA125DB2813C0CA065EA3D731D42B00B7A9FAFDC136CCFF68ECEE638BA2EF94A38F3655BB1FC8F705CDD87CF23E718D6FAADAFD19168E0AE3346338D625AE314C7CB58F921232FBA6995BCFDD21AEF551557FBAB4736AA38924B26F36503AF9A3A95E29F26002C33F89CC58BE4BCD900E1FA2741E3E89A8BC8D78E2C704F03E3547B2F43AC6E4572778FD2D8EEBF7A859926101F04A0AB77E89DCF5FC1029EF0016024B89EFBE5D00FE413AAF83DB319341765B4E92EF4297A97215A519C2C749E60D603916112FFD52DC14F2323557633803F3EEFD49C73377043F52C2CE1141617149DB4398323767AA19AB6739F005E09798248F51DC6FE00EE357DD80D3817F9769EBAB517CF040143C6AB018B10CB818F80EA61ED0781905CACF0EFC4CBBEAC45434A9468613983AC15F073691AF8A6815E9C1E4CF8F44069FAC5261C5D2EAABF07BE6AD0593A3F05C0D724D46BA18C2AC77E8CE93C1DB804F03B746B7F4420D9F21E07EE02BC0BA0CCADE0F5C8399391CA851F641E076E072329864DA1605463B6A10780CB38E6E2F701D8D7D97EF13E702D7037F8F460B6355EAE741E06D789E7FB004933675A04AA186819B31397C6B896FA516029D98E4D64B22BD1DA9426703C08F7DEA20B380B7535A0ACDF59C3B0CEC06BE019CA531B0933E4C8EE100A51948AE6078252916FADA8829803C51A191AF005FC32CA298AFF6ADA9632D8E628017A77874EE8E3A6162F402BFA8D0A8039852E8FD6AC786B10E938CF27205BD6F4F628EE18B94AED22D7E0E621226B40C7AF368053E4EE9CA287B7E6173332EDC8149B4745DF477C087D53689B3391A4DB86C720B0DAEA774D07191A3C0F96A87D4D952663839D8A85BCE2EC7977F9B6C54EC0A851EE0670E3BED9EEEDCC1FB1D51E73AD5B7B75CE888D12E99CE17CAE95B5D04E93F17519A2B5917B2FAC53ED56D6678840614A9FEACF8924DAAD7CCB0BEDA3B77A569C47788F3DFAB5E33C37E71BEB81E07586E1D1F45F7BACF1AF67ECC67D4E30036AFAB03648A494CB26A91B28B6567D5F0A573D07570596176E40045C3774ED7011670EA3DBFE23F2DC2E8EDF538408B389EA77ACD2C6DF5C40007556FB9E1AFD5F472175762B66D9B2D6EFF05F19332E7D4F877AE7F6FF66327EF8FB53F015BB50F288AA2288AA2288A62F83FEC37068C6750398B0000000049454E44AE426082" decoded, err := hex.DecodeString(img) @@ -57,7 +57,7 @@ func (h *AvatarsHandler) Handler(s *svc) http.Handler { log.Error().Err(err).Msg("error decoding string") w.WriteHeader(http.StatusInternalServerError) } - w.Header().Set("Content-Type", "image/png") + w.Header().Set(HeaderContentType, "image/png") if _, err := w.Write(decoded); err != nil { log.Error().Err(err).Msg("error writing data response") } diff --git a/internal/http/services/owncloud/ocdav/copy.go b/internal/http/services/owncloud/ocdav/copy.go index 67c1455fc3..467be41c58 100644 --- a/internal/http/services/owncloud/ocdav/copy.go +++ b/internal/http/services/owncloud/ocdav/copy.go @@ -23,6 +23,7 @@ import ( "fmt" "net/http" "path" + "strconv" "strings" gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" @@ -32,23 +33,26 @@ import ( "github.com/cs3org/reva/internal/http/services/datagateway" "github.com/cs3org/reva/pkg/appctx" "github.com/cs3org/reva/pkg/rhttp" + "github.com/rs/zerolog" "go.opencensus.io/trace" ) -func (s *svc) handleCopy(w http.ResponseWriter, r *http.Request, ns string) { +type copy struct { + sourceInfo *provider.ResourceInfo + destination *provider.Reference + depth string + successCode int +} + +type intermediateDirRefFunc func() (*provider.Reference, *rpc.Status, error) + +func (s *svc) handlePathCopy(w http.ResponseWriter, r *http.Request, ns string) { ctx := r.Context() ctx, span := trace.StartSpan(ctx, "head") defer span.End() src := path.Join(ns, r.URL.Path) - dstHeader := r.Header.Get("Destination") - overwrite := r.Header.Get("Overwrite") - depth := r.Header.Get("Depth") - if depth == "" { - depth = "infinity" - } - - dst, err := extractDestination(dstHeader, r.Context().Value(ctxKeyBaseURI).(string)) + dst, err := extractDestination(r) if err != nil { w.WriteHeader(http.StatusBadRequest) return @@ -56,32 +60,20 @@ func (s *svc) handleCopy(w http.ResponseWriter, r *http.Request, ns string) { dst = path.Join(ns, dst) sublog := appctx.GetLogger(ctx).With().Str("src", src).Str("dst", dst).Logger() - sublog.Debug().Str("overwrite", overwrite).Str("depth", depth).Msg("copy") - overwrite = strings.ToUpper(overwrite) - if overwrite == "" { - overwrite = "T" - } + srcRef := &provider.Reference{Path: src} - if overwrite != "T" && overwrite != "F" { - w.WriteHeader(http.StatusBadRequest) - m := fmt.Sprintf("Overwrite header is set to incorrect value %v", overwrite) - b, err := Marshal(exception{ - code: SabredavBadRequest, - message: m, - }) - HandleWebdavError(&sublog, w, b, err) - return + // check dst exists + dstRef := &provider.Reference{Path: dst} + + intermediateDirRefFunc := func() (*provider.Reference, *rpc.Status, error) { + intermediateDir := path.Dir(dst) + ref := &provider.Reference{Path: intermediateDir} + return ref, &rpc.Status{Code: rpc.Code_CODE_OK}, nil } - if depth != "infinity" && depth != "0" { - w.WriteHeader(http.StatusBadRequest) - m := fmt.Sprintf("Depth header is set to incorrect value %v", depth) - b, err := Marshal(exception{ - code: SabredavBadRequest, - message: m, - }) - HandleWebdavError(&sublog, w, b, err) + cp := s.prepareCopy(ctx, w, r, srcRef, dstRef, intermediateDirRefFunc, &sublog) + if cp == nil { return } @@ -92,108 +84,26 @@ func (s *svc) handleCopy(w http.ResponseWriter, r *http.Request, ns string) { return } - // check src exists - ref := &provider.Reference{Path: src} - srcStatReq := &provider.StatRequest{Ref: ref} - srcStatRes, err := client.Stat(ctx, srcStatReq) - if err != nil { - sublog.Error().Err(err).Msg("error sending grpc stat request") - w.WriteHeader(http.StatusInternalServerError) - return - } - - if srcStatRes.Status.Code != rpc.Code_CODE_OK { - if srcStatRes.Status.Code == rpc.Code_CODE_NOT_FOUND { - w.WriteHeader(http.StatusNotFound) - m := fmt.Sprintf("Resource %v not found", srcStatReq.Ref.Path) - b, err := Marshal(exception{ - code: SabredavNotFound, - message: m, - }) - HandleWebdavError(&sublog, w, b, err) - } - HandleErrorStatus(&sublog, w, srcStatRes.Status) - return - } - - // check dst exists - ref = &provider.Reference{Path: dst} - dstStatReq := &provider.StatRequest{Ref: ref} - dstStatRes, err := client.Stat(ctx, dstStatReq) - if err != nil { - sublog.Error().Err(err).Msg("error sending grpc stat request") - w.WriteHeader(http.StatusInternalServerError) - return - } - if dstStatRes.Status.Code != rpc.Code_CODE_OK && dstStatRes.Status.Code != rpc.Code_CODE_NOT_FOUND { - HandleErrorStatus(&sublog, w, srcStatRes.Status) - return - } - - successCode := http.StatusCreated // 201 if new resource was created, see https://tools.ietf.org/html/rfc4918#section-9.8.5 - if dstStatRes.Status.Code == rpc.Code_CODE_OK { - successCode = http.StatusNoContent // 204 if target already existed, see https://tools.ietf.org/html/rfc4918#section-9.8.5 - - if overwrite == "F" { - sublog.Warn().Str("overwrite", overwrite).Msg("dst already exists") - w.WriteHeader(http.StatusPreconditionFailed) - m := fmt.Sprintf("Could not overwrite Resource %v", dst) - b, err := Marshal(exception{ - code: SabredavPreconditionFailed, - message: m, - }) - HandleWebdavError(&sublog, w, b, err) // 412, see https://tools.ietf.org/html/rfc4918#section-9.8.5 - return - } - - } else { - // check if an intermediate path / the parent exists - intermediateDir := path.Dir(dst) - ref = &provider.Reference{Path: intermediateDir} - intStatReq := &provider.StatRequest{Ref: ref} - intStatRes, err := client.Stat(ctx, intStatReq) - if err != nil { - sublog.Error().Err(err).Msg("error sending grpc stat request") - w.WriteHeader(http.StatusInternalServerError) - return - } - if intStatRes.Status.Code != rpc.Code_CODE_OK { - if intStatRes.Status.Code == rpc.Code_CODE_NOT_FOUND { - // 409 if intermediate dir is missing, see https://tools.ietf.org/html/rfc4918#section-9.8.5 - sublog.Debug().Str("parent", intermediateDir).Interface("status", intStatRes.Status).Msg("conflict") - w.WriteHeader(http.StatusConflict) - } else { - HandleErrorStatus(&sublog, w, srcStatRes.Status) - } - return - } - // TODO what if intermediate is a file? - } - - err = s.descend(ctx, w, client, srcStatRes.Info, dst, depth == "infinity") - if err != nil { - sublog.Error().Err(err).Str("depth", depth).Msg("error descending directory") + if err := s.executePathCopy(ctx, client, w, r, cp); err != nil { + sublog.Error().Err(err).Str("depth", cp.depth).Msg("error executing path copy") w.WriteHeader(http.StatusInternalServerError) - return } - w.WriteHeader(successCode) + w.WriteHeader(cp.successCode) } -func (s *svc) descend(ctx context.Context, w http.ResponseWriter, client gateway.GatewayAPIClient, src *provider.ResourceInfo, dst string, recurse bool) error { +func (s *svc) executePathCopy(ctx context.Context, client gateway.GatewayAPIClient, w http.ResponseWriter, r *http.Request, cp *copy) error { log := appctx.GetLogger(ctx) - log.Debug().Str("src", src.Path).Str("dst", dst).Msg("descending") - if src.Type == provider.ResourceType_RESOURCE_TYPE_CONTAINER { + log.Debug().Str("src", cp.sourceInfo.Path).Str("dst", cp.destination.Path).Msg("descending") + if cp.sourceInfo.Type == provider.ResourceType_RESOURCE_TYPE_CONTAINER { // create dir createReq := &provider.CreateContainerRequest{ - Ref: &provider.Reference{Path: dst}, + Ref: cp.destination, } createRes, err := client.CreateContainer(ctx, createReq) if err != nil { log.Error().Err(err).Msg("error performing create container grpc request") - w.WriteHeader(http.StatusInternalServerError) return err } - if createRes.Status.Code != rpc.Code_CODE_OK { if createRes.Status.Code == rpc.Code_CODE_PERMISSION_DENIED { w.WriteHeader(http.StatusForbidden) @@ -209,25 +119,26 @@ func (s *svc) descend(ctx context.Context, w http.ResponseWriter, client gateway // TODO: also copy properties: https://tools.ietf.org/html/rfc4918#section-9.8.2 - if !recurse { + if cp.depth != "infinity" { return nil } // descend for children listReq := &provider.ListContainerRequest{ - Ref: &provider.Reference{Path: src.Path}, + Ref: &provider.Reference{Path: cp.sourceInfo.Path}, } res, err := client.ListContainer(ctx, listReq) if err != nil { return err } if res.Status.Code != rpc.Code_CODE_OK { - return fmt.Errorf("status code %d", res.Status.Code) + w.WriteHeader(http.StatusInternalServerError) + return nil } for i := range res.Infos { - childDst := path.Join(dst, path.Base(res.Infos[i].Path)) - err := s.descend(ctx, w, client, res.Infos[i], childDst, recurse) + childDst := &provider.Reference{Path: path.Join(cp.destination.Path, path.Base(res.Infos[i].Path))} + err := s.executePathCopy(ctx, client, w, r, ©{sourceInfo: res.Infos[i], destination: childDst, depth: cp.depth, successCode: cp.successCode}) if err != nil { return err } @@ -239,7 +150,7 @@ func (s *svc) descend(ctx context.Context, w http.ResponseWriter, client gateway // 1. get download url dReq := &provider.InitiateFileDownloadRequest{ - Ref: &provider.Reference{Path: src.Path}, + Ref: &provider.Reference{Path: cp.sourceInfo.Path}, } dRes, err := client.InitiateFileDownload(ctx, dReq) @@ -261,13 +172,13 @@ func (s *svc) descend(ctx context.Context, w http.ResponseWriter, client gateway // 2. get upload url uReq := &provider.InitiateFileUploadRequest{ - Ref: &provider.Reference{Path: dst}, + Ref: cp.destination, Opaque: &typespb.Opaque{ Map: map[string]*typespb.OpaqueEntry{ "Upload-Length": { Decoder: "plain", // TODO: handle case where size is not known in advance - Value: []byte(fmt.Sprintf("%d", src.GetSize())), + Value: []byte(strconv.FormatUint(cp.sourceInfo.GetSize(), 10)), }, }, }, @@ -281,12 +192,13 @@ func (s *svc) descend(ctx context.Context, w http.ResponseWriter, client gateway if uRes.Status.Code != rpc.Code_CODE_OK { if uRes.Status.Code == rpc.Code_CODE_PERMISSION_DENIED { w.WriteHeader(http.StatusForbidden) - m := fmt.Sprintf("Permission denied to create %v", uReq.Ref.Path) + m := fmt.Sprintf("Permissions denied to create %v", uReq.Ref.Path) b, err := Marshal(exception{ code: SabredavPermissionDenied, message: m, }) HandleWebdavError(log, w, b, err) + return nil } HandleErrorStatus(log, w, uRes.Status) return nil @@ -318,7 +230,7 @@ func (s *svc) descend(ctx context.Context, w http.ResponseWriter, client gateway // 4. do upload - if src.GetSize() > 0 { + if cp.sourceInfo.GetSize() > 0 { httpUploadReq, err := rhttp.NewRequest(ctx, "PUT", uploadEP, httpDownloadRes.Body) if err != nil { return err @@ -337,3 +249,147 @@ func (s *svc) descend(ctx context.Context, w http.ResponseWriter, client gateway } return nil } + +func (s *svc) prepareCopy(ctx context.Context, w http.ResponseWriter, r *http.Request, srcRef, dstRef *provider.Reference, intermediateDirRef intermediateDirRefFunc, log *zerolog.Logger) *copy { + overwrite, err := extractOverwrite(w, r) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + m := fmt.Sprintf("Overwrite header is set to incorrect value %v", overwrite) + b, err := Marshal(exception{ + code: SabredavBadRequest, + message: m, + }) + HandleWebdavError(log, w, b, err) + return nil + } + depth, err := extractDepth(w, r) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + m := fmt.Sprintf("Depth header is set to incorrect value %v", depth) + b, err := Marshal(exception{ + code: SabredavBadRequest, + message: m, + }) + HandleWebdavError(log, w, b, err) + return nil + } + + log.Debug().Str("overwrite", overwrite).Str("depth", depth).Msg("copy") + + client, err := s.getClient() + if err != nil { + log.Error().Err(err).Msg("error getting grpc client") + w.WriteHeader(http.StatusInternalServerError) + return nil + } + + srcStatReq := &provider.StatRequest{Ref: srcRef} + srcStatRes, err := client.Stat(ctx, srcStatReq) + if err != nil { + log.Error().Err(err).Msg("error sending grpc stat request") + w.WriteHeader(http.StatusInternalServerError) + return nil + } + + if srcStatRes.Status.Code != rpc.Code_CODE_OK { + if srcStatRes.Status.Code == rpc.Code_CODE_NOT_FOUND { + w.WriteHeader(http.StatusNotFound) + m := fmt.Sprintf("Resource %v not found", srcStatReq.Ref.Path) + b, err := Marshal(exception{ + code: SabredavNotFound, + message: m, + }) + HandleWebdavError(log, w, b, err) + } + HandleErrorStatus(log, w, srcStatRes.Status) + return nil + } + + dstStatReq := &provider.StatRequest{Ref: dstRef} + dstStatRes, err := client.Stat(ctx, dstStatReq) + if err != nil { + log.Error().Err(err).Msg("error sending grpc stat request") + w.WriteHeader(http.StatusInternalServerError) + return nil + } + if dstStatRes.Status.Code != rpc.Code_CODE_OK && dstStatRes.Status.Code != rpc.Code_CODE_NOT_FOUND { + HandleErrorStatus(log, w, srcStatRes.Status) + return nil + } + + successCode := http.StatusCreated // 201 if new resource was created, see https://tools.ietf.org/html/rfc4918#section-9.8.5 + if dstStatRes.Status.Code == rpc.Code_CODE_OK { + successCode = http.StatusNoContent // 204 if target already existed, see https://tools.ietf.org/html/rfc4918#section-9.8.5 + + if overwrite == "F" { + log.Warn().Str("overwrite", overwrite).Msg("dst already exists") + w.WriteHeader(http.StatusPreconditionFailed) + m := fmt.Sprintf("Could not overwrite Resource %v", dstRef.Path) + b, err := Marshal(exception{ + code: SabredavPreconditionFailed, + message: m, + }) + HandleWebdavError(log, w, b, err) // 412, see https://tools.ietf.org/html/rfc4918#section-9.8.5 + return nil + } + + } else { + // check if an intermediate path / the parent exists + intermediateRef, status, err := intermediateDirRef() + if err != nil { + log.Error().Err(err).Msg("error sending a grpc request") + w.WriteHeader(http.StatusInternalServerError) + return nil + } + + if status.Code != rpc.Code_CODE_OK { + HandleErrorStatus(log, w, status) + return nil + } + intStatReq := &provider.StatRequest{Ref: intermediateRef} + intStatRes, err := client.Stat(ctx, intStatReq) + if err != nil { + log.Error().Err(err).Msg("error sending grpc stat request") + w.WriteHeader(http.StatusInternalServerError) + return nil + } + if intStatRes.Status.Code != rpc.Code_CODE_OK { + if intStatRes.Status.Code == rpc.Code_CODE_NOT_FOUND { + // 409 if intermediate dir is missing, see https://tools.ietf.org/html/rfc4918#section-9.8.5 + log.Debug().Interface("parent", intermediateRef).Interface("status", intStatRes.Status).Msg("conflict") + w.WriteHeader(http.StatusConflict) + } else { + HandleErrorStatus(log, w, srcStatRes.Status) + } + return nil + } + // TODO what if intermediate is a file? + } + + return ©{sourceInfo: srcStatRes.Info, depth: depth, successCode: successCode, destination: dstRef} +} + +func extractOverwrite(w http.ResponseWriter, r *http.Request) (string, error) { + overwrite := r.Header.Get(HeaderOverwrite) + overwrite = strings.ToUpper(overwrite) + if overwrite == "" { + overwrite = "T" + } + + if overwrite != "T" && overwrite != "F" { + return "", errInvalidValue + } + + return overwrite, nil +} + +func extractDepth(w http.ResponseWriter, r *http.Request) (string, error) { + depth := r.Header.Get(HeaderDepth) + if depth == "" { + depth = "infinity" + } + if depth != "infinity" && depth != "0" { + return "", errInvalidValue + } + return depth, nil +} diff --git a/internal/http/services/owncloud/ocdav/delete.go b/internal/http/services/owncloud/ocdav/delete.go index 29ff9de84d..8b221a10c0 100644 --- a/internal/http/services/owncloud/ocdav/delete.go +++ b/internal/http/services/owncloud/ocdav/delete.go @@ -19,6 +19,7 @@ package ocdav import ( + "context" "fmt" "net/http" "path" @@ -26,52 +27,54 @@ import ( rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" "github.com/cs3org/reva/pkg/appctx" + "github.com/rs/zerolog" "go.opencensus.io/trace" ) -func (s *svc) handleDelete(w http.ResponseWriter, r *http.Request, ns string) { +func (s *svc) handlePathDelete(w http.ResponseWriter, r *http.Request, ns string) { ctx := r.Context() - ctx, span := trace.StartSpan(ctx, "head") + ctx, span := trace.StartSpan(ctx, "delete") defer span.End() fn := path.Join(ns, r.URL.Path) sublog := appctx.GetLogger(ctx).With().Str("path", fn).Logger() + ref := &provider.Reference{Path: fn} + s.handleDelete(ctx, w, r, ref, sublog) +} +func (s *svc) handleDelete(ctx context.Context, w http.ResponseWriter, r *http.Request, ref *provider.Reference, log zerolog.Logger) { client, err := s.getClient() if err != nil { - sublog.Error().Err(err).Msg("error getting grpc client") + log.Error().Err(err).Msg("error getting grpc client") w.WriteHeader(http.StatusInternalServerError) return } - ref := &provider.Reference{Path: fn} req := &provider.DeleteRequest{Ref: ref} res, err := client.Delete(ctx, req) if err != nil { - sublog.Error().Err(err).Msg("error performing delete grpc request") + log.Error().Err(err).Msg("error performing delete grpc request") w.WriteHeader(http.StatusInternalServerError) return - } - - if res.Status.Code != rpc.Code_CODE_OK { + } else if res.Status.Code != rpc.Code_CODE_OK { if res.Status.Code == rpc.Code_CODE_NOT_FOUND { w.WriteHeader(http.StatusNotFound) - m := fmt.Sprintf("Resource %v not found", fn) + m := fmt.Sprintf("Resource %v not found", ref.Path) b, err := Marshal(exception{ code: SabredavNotFound, message: m, }) - HandleWebdavError(&sublog, w, b, err) + HandleWebdavError(&log, w, b, err) } if res.Status.Code == rpc.Code_CODE_PERMISSION_DENIED { w.WriteHeader(http.StatusForbidden) - m := fmt.Sprintf("Permission denied to delete %v", fn) + m := fmt.Sprintf("Permission denied to delete %v", ref.Path) b, err := Marshal(exception{ code: SabredavPermissionDenied, message: m, }) - HandleWebdavError(&sublog, w, b, err) + HandleWebdavError(&log, w, b, err) } if res.Status.Code == rpc.Code_CODE_INTERNAL && res.Status.Message == "can't delete mount path" { w.WriteHeader(http.StatusForbidden) @@ -79,9 +82,9 @@ func (s *svc) handleDelete(w http.ResponseWriter, r *http.Request, ns string) { code: SabredavPermissionDenied, message: res.Status.Message, }) - HandleWebdavError(&sublog, w, b, err) + HandleWebdavError(&log, w, b, err) } - HandleErrorStatus(&sublog, w, res.Status) + HandleErrorStatus(&log, w, res.Status) return } w.WriteHeader(http.StatusNoContent) diff --git a/internal/http/services/owncloud/ocdav/get.go b/internal/http/services/owncloud/ocdav/get.go index d462d8fa1c..8674ed62a5 100644 --- a/internal/http/services/owncloud/ocdav/get.go +++ b/internal/http/services/owncloud/ocdav/get.go @@ -19,6 +19,7 @@ package ocdav import ( + "context" "fmt" "io" "net/http" @@ -29,6 +30,7 @@ import ( "github.com/cs3org/reva/internal/grpc/services/storageprovider" "github.com/cs3org/reva/internal/http/services/datagateway" + "github.com/rs/zerolog" "go.opencensus.io/trace" rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" @@ -38,7 +40,7 @@ import ( "github.com/cs3org/reva/pkg/utils" ) -func (s *svc) handleGet(w http.ResponseWriter, r *http.Request, ns string) { +func (s *svc) handlePathGet(w http.ResponseWriter, r *http.Request, ns string) { ctx := r.Context() ctx, span := trace.StartSpan(ctx, "get") defer span.End() @@ -47,75 +49,69 @@ func (s *svc) handleGet(w http.ResponseWriter, r *http.Request, ns string) { sublog := appctx.GetLogger(ctx).With().Str("path", fn).Str("svc", "ocdav").Str("handler", "get").Logger() + ref := &provider.Reference{Path: fn} + s.handleGet(ctx, w, r, ref, "simple", sublog) +} + +func (s *svc) handleGet(ctx context.Context, w http.ResponseWriter, r *http.Request, ref *provider.Reference, dlProtocol string, log zerolog.Logger) { client, err := s.getClient() if err != nil { - sublog.Error().Err(err).Msg("error getting grpc client") + log.Error().Err(err).Msg("error getting grpc client") w.WriteHeader(http.StatusInternalServerError) return } - sReq := &provider.StatRequest{ - Ref: &provider.Reference{Path: fn}, - } + sReq := &provider.StatRequest{Ref: ref} sRes, err := client.Stat(ctx, sReq) - if err != nil { - sublog.Error().Err(err).Msg("error sending grpc stat request") + switch { + case err != nil: + log.Error().Err(err).Msg("error sending grpc stat request") w.WriteHeader(http.StatusInternalServerError) return - } - - if sRes.Status.Code != rpc.Code_CODE_OK { - HandleErrorStatus(&sublog, w, sRes.Status) + case sRes.Status.Code != rpc.Code_CODE_OK: + HandleErrorStatus(&log, w, sRes.Status) return - } - - info := sRes.Info - if info.Type == provider.ResourceType_RESOURCE_TYPE_CONTAINER { - sublog.Warn().Msg("resource is a folder and cannot be downloaded") + case sRes.Info.Type == provider.ResourceType_RESOURCE_TYPE_CONTAINER: + log.Warn().Msg("resource is a folder and cannot be downloaded") w.WriteHeader(http.StatusNotImplemented) return } - dReq := &provider.InitiateFileDownloadRequest{ - Ref: &provider.Reference{Path: fn}, - } - + dReq := &provider.InitiateFileDownloadRequest{Ref: ref} dRes, err := client.InitiateFileDownload(ctx, dReq) if err != nil { - sublog.Error().Err(err).Msg("error initiating file download") + log.Error().Err(err).Msg("error initiating file download") w.WriteHeader(http.StatusInternalServerError) return - } - - if dRes.Status.Code != rpc.Code_CODE_OK { - HandleErrorStatus(&sublog, w, dRes.Status) + } else if dRes.Status.Code != rpc.Code_CODE_OK { + HandleErrorStatus(&log, w, dRes.Status) return } var ep, token string for _, p := range dRes.Protocols { - if p.Protocol == "simple" { + if p.Protocol == dlProtocol { ep, token = p.DownloadEndpoint, p.Token } } - httpReq, err := rhttp.NewRequest(ctx, "GET", ep, nil) + httpReq, err := rhttp.NewRequest(ctx, http.MethodGet, ep, nil) if err != nil { - sublog.Error().Err(err).Msg("error creating http request") + log.Error().Err(err).Msg("error creating http request") w.WriteHeader(http.StatusInternalServerError) return } httpReq.Header.Set(datagateway.TokenTransportHeader, token) - if r.Header.Get("Range") != "" { - httpReq.Header.Set("Range", r.Header.Get("Range")) + if r.Header.Get(HeaderRange) != "" { + httpReq.Header.Set(HeaderRange, r.Header.Get(HeaderRange)) } httpClient := s.client httpRes, err := httpClient.Do(httpReq) if err != nil { - sublog.Error().Err(err).Msg("error performing http request") + log.Error().Err(err).Msg("error performing http request") w.WriteHeader(http.StatusInternalServerError) return } @@ -126,37 +122,39 @@ func (s *svc) handleGet(w http.ResponseWriter, r *http.Request, ns string) { return } - w.Header().Set("Content-Type", info.MimeType) - w.Header().Set("Content-Disposition", "attachment; filename*=UTF-8''"+ + info := sRes.Info + + w.Header().Set(HeaderContentType, info.MimeType) + w.Header().Set(HeaderContentDisposistion, "attachment; filename*=UTF-8''"+ path.Base(info.Path)+"; filename=\""+path.Base(info.Path)+"\"") - w.Header().Set("ETag", info.Etag) - w.Header().Set("OC-FileId", wrapResourceID(info.Id)) - w.Header().Set("OC-ETag", info.Etag) + w.Header().Set(HeaderETag, info.Etag) + w.Header().Set(HeaderOCFileID, wrapResourceID(info.Id)) + w.Header().Set(HeaderOCETag, info.Etag) t := utils.TSToTime(info.Mtime).UTC() lastModifiedString := t.Format(time.RFC1123Z) - w.Header().Set("Last-Modified", lastModifiedString) + w.Header().Set(HeaderLastModified, lastModifiedString) if httpRes.StatusCode == http.StatusPartialContent { - w.Header().Set("Content-Range", httpRes.Header.Get("Content-Range")) - w.Header().Set("Content-Length", httpRes.Header.Get("Content-Length")) + w.Header().Set(HeaderContentRange, httpRes.Header.Get(HeaderContentRange)) + w.Header().Set(HeaderContentLength, httpRes.Header.Get(HeaderContentLength)) w.WriteHeader(http.StatusPartialContent) } else { - w.Header().Set("Content-Length", strconv.FormatUint(info.Size, 10)) + w.Header().Set(HeaderContentLength, strconv.FormatUint(info.Size, 10)) } if info.Checksum != nil { - w.Header().Set("OC-Checksum", fmt.Sprintf("%s:%s", strings.ToUpper(string(storageprovider.GRPC2PKGXS(info.Checksum.Type))), info.Checksum.Sum)) + w.Header().Set(HeaderOCChecksum, fmt.Sprintf("%s:%s", strings.ToUpper(string(storageprovider.GRPC2PKGXS(info.Checksum.Type))), info.Checksum.Sum)) } var c int64 if c, err = io.Copy(w, httpRes.Body); err != nil { - sublog.Error().Err(err).Msg("error finishing copying data to response") + log.Error().Err(err).Msg("error finishing copying data to response") } - if httpRes.Header.Get("Content-Length") != "" { - i, err := strconv.ParseInt(httpRes.Header.Get("Content-Length"), 10, 64) + if httpRes.Header.Get(HeaderContentLength) != "" { + i, err := strconv.ParseInt(httpRes.Header.Get(HeaderContentLength), 10, 64) if err != nil { - sublog.Error().Err(err).Str("content-length", httpRes.Header.Get("Content-Length")).Msg("invalid content length in datagateway response") + log.Error().Err(err).Str("content-length", httpRes.Header.Get(HeaderContentLength)).Msg("invalid content length in datagateway response") } if i != c { - sublog.Error().Int64("content-length", i).Int64("transferred-bytes", c).Msg("content length vs transferred bytes mismatch") + log.Error().Int64("content-length", i).Int64("transferred-bytes", c).Msg("content length vs transferred bytes mismatch") } } // TODO we need to send the If-Match etag in the GET to the datagateway to prevent race conditions between stating and reading the file diff --git a/internal/http/services/owncloud/ocdav/head.go b/internal/http/services/owncloud/ocdav/head.go index f70d4b95d8..6755d5e15b 100644 --- a/internal/http/services/owncloud/ocdav/head.go +++ b/internal/http/services/owncloud/ocdav/head.go @@ -19,6 +19,7 @@ package ocdav import ( + "context" "fmt" "net/http" "path" @@ -31,10 +32,11 @@ import ( "github.com/cs3org/reva/internal/grpc/services/storageprovider" "github.com/cs3org/reva/pkg/appctx" "github.com/cs3org/reva/pkg/utils" + "github.com/rs/zerolog" "go.opencensus.io/trace" ) -func (s *svc) handleHead(w http.ResponseWriter, r *http.Request, ns string) { +func (s *svc) handlePathHead(w http.ResponseWriter, r *http.Request, ns string) { ctx := r.Context() ctx, span := trace.StartSpan(ctx, "head") defer span.End() @@ -43,41 +45,45 @@ func (s *svc) handleHead(w http.ResponseWriter, r *http.Request, ns string) { sublog := appctx.GetLogger(ctx).With().Str("path", fn).Logger() + ref := &provider.Reference{Path: fn} + s.handleHead(ctx, w, r, ref, sublog) +} + +func (s *svc) handleHead(ctx context.Context, w http.ResponseWriter, r *http.Request, ref *provider.Reference, log zerolog.Logger) { client, err := s.getClient() if err != nil { - sublog.Error().Err(err).Msg("error getting grpc client") + log.Error().Err(err).Msg("error getting grpc client") w.WriteHeader(http.StatusInternalServerError) return } - ref := &provider.Reference{Path: fn} req := &provider.StatRequest{Ref: ref} res, err := client.Stat(ctx, req) if err != nil { - sublog.Error().Err(err).Msg("error sending grpc stat request") + log.Error().Err(err).Msg("error sending grpc stat request") w.WriteHeader(http.StatusInternalServerError) return } if res.Status.Code != rpc.Code_CODE_OK { - HandleErrorStatus(&sublog, w, res.Status) + HandleErrorStatus(&log, w, res.Status) return } info := res.Info - w.Header().Set("Content-Type", info.MimeType) - w.Header().Set("ETag", info.Etag) - w.Header().Set("OC-FileId", wrapResourceID(info.Id)) - w.Header().Set("OC-ETag", info.Etag) + w.Header().Set(HeaderContentType, info.MimeType) + w.Header().Set(HeaderETag, info.Etag) + w.Header().Set(HeaderOCFileID, wrapResourceID(info.Id)) + w.Header().Set(HeaderOCETag, info.Etag) if info.Checksum != nil { - w.Header().Set("OC-Checksum", fmt.Sprintf("%s:%s", strings.ToUpper(string(storageprovider.GRPC2PKGXS(info.Checksum.Type))), info.Checksum.Sum)) + w.Header().Set(HeaderOCChecksum, fmt.Sprintf("%s:%s", strings.ToUpper(string(storageprovider.GRPC2PKGXS(info.Checksum.Type))), info.Checksum.Sum)) } t := utils.TSToTime(info.Mtime).UTC() lastModifiedString := t.Format(time.RFC1123Z) - w.Header().Set("Last-Modified", lastModifiedString) - w.Header().Set("Content-Length", strconv.FormatUint(info.Size, 10)) + w.Header().Set(HeaderLastModified, lastModifiedString) + w.Header().Set(HeaderContentLength, strconv.FormatUint(info.Size, 10)) if info.Type != provider.ResourceType_RESOURCE_TYPE_CONTAINER { - w.Header().Set("Accept-Ranges", "bytes") + w.Header().Set(HeaderAcceptRanges, "bytes") } w.WriteHeader(http.StatusOK) } diff --git a/internal/http/services/owncloud/ocdav/mkcol.go b/internal/http/services/owncloud/ocdav/mkcol.go index 573da3b26f..ba0389ba6e 100644 --- a/internal/http/services/owncloud/ocdav/mkcol.go +++ b/internal/http/services/owncloud/ocdav/mkcol.go @@ -19,18 +19,19 @@ package ocdav import ( + "context" "fmt" - "io" "net/http" "path" rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" "github.com/cs3org/reva/pkg/appctx" + "github.com/rs/zerolog" "go.opencensus.io/trace" ) -func (s *svc) handleMkcol(w http.ResponseWriter, r *http.Request, ns string) { +func (s *svc) handlePathMkcol(w http.ResponseWriter, r *http.Request, ns string) { ctx := r.Context() ctx, span := trace.StartSpan(ctx, "mkcol") defer span.End() @@ -39,27 +40,29 @@ func (s *svc) handleMkcol(w http.ResponseWriter, r *http.Request, ns string) { sublog := appctx.GetLogger(ctx).With().Str("path", fn).Logger() - buf := make([]byte, 1) - _, err := r.Body.Read(buf) - if err != io.EOF { - sublog.Error().Err(err).Msg("error reading request body") + ref := &provider.Reference{Path: fn} + + s.handleMkcol(ctx, w, r, ref, sublog) +} + +func (s *svc) handleMkcol(ctx context.Context, w http.ResponseWriter, r *http.Request, ref *provider.Reference, log zerolog.Logger) { + if r.Body != http.NoBody { w.WriteHeader(http.StatusUnsupportedMediaType) return } client, err := s.getClient() if err != nil { - sublog.Error().Err(err).Msg("error getting grpc client") + log.Error().Err(err).Msg("error getting grpc client") w.WriteHeader(http.StatusInternalServerError) return } // check fn exists - ref := &provider.Reference{Path: fn} statReq := &provider.StatRequest{Ref: ref} statRes, err := client.Stat(ctx, statReq) if err != nil { - sublog.Error().Err(err).Msg("error sending a grpc stat request") + log.Error().Err(err).Msg("error sending a grpc stat request") w.WriteHeader(http.StatusInternalServerError) return } @@ -68,7 +71,7 @@ func (s *svc) handleMkcol(w http.ResponseWriter, r *http.Request, ns string) { if statRes.Status.Code == rpc.Code_CODE_OK { w.WriteHeader(http.StatusMethodNotAllowed) // 405 if it already exists } else { - HandleErrorStatus(&sublog, w, statRes.Status) + HandleErrorStatus(&log, w, statRes.Status) } return } @@ -76,7 +79,7 @@ func (s *svc) handleMkcol(w http.ResponseWriter, r *http.Request, ns string) { req := &provider.CreateContainerRequest{Ref: ref} res, err := client.CreateContainer(ctx, req) if err != nil { - sublog.Error().Err(err).Msg("error sending create container grpc request") + log.Error().Err(err).Msg("error sending create container grpc request") w.WriteHeader(http.StatusInternalServerError) return } @@ -84,17 +87,17 @@ func (s *svc) handleMkcol(w http.ResponseWriter, r *http.Request, ns string) { case rpc.Code_CODE_OK: w.WriteHeader(http.StatusCreated) case rpc.Code_CODE_NOT_FOUND: - sublog.Debug().Str("path", fn).Interface("status", statRes.Status).Msg("conflict") + log.Debug().Str("path", ref.Path).Interface("status", statRes.Status).Msg("conflict") w.WriteHeader(http.StatusConflict) case rpc.Code_CODE_PERMISSION_DENIED: w.WriteHeader(http.StatusForbidden) - m := fmt.Sprintf("Permission denied to create %v", fn) + m := fmt.Sprintf("Permission denied to create %v", ref.Path) b, err := Marshal(exception{ code: SabredavPermissionDenied, message: m, }) - HandleWebdavError(&sublog, w, b, err) + HandleWebdavError(&log, w, b, err) default: - HandleErrorStatus(&sublog, w, res.Status) + HandleErrorStatus(&log, w, res.Status) } } diff --git a/internal/http/services/owncloud/ocdav/move.go b/internal/http/services/owncloud/ocdav/move.go index 0cb677dfc7..4913137574 100644 --- a/internal/http/services/owncloud/ocdav/move.go +++ b/internal/http/services/owncloud/ocdav/move.go @@ -19,6 +19,7 @@ package ocdav import ( + "context" "fmt" "net/http" "path" @@ -27,27 +28,39 @@ import ( rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" "github.com/cs3org/reva/pkg/appctx" + "github.com/rs/zerolog" "go.opencensus.io/trace" ) -func (s *svc) handleMove(w http.ResponseWriter, r *http.Request, ns string) { +func (s *svc) handlePathMove(w http.ResponseWriter, r *http.Request, ns string) { ctx := r.Context() ctx, span := trace.StartSpan(ctx, "move") defer span.End() - src := path.Join(ns, r.URL.Path) - dstHeader := r.Header.Get("Destination") - overwrite := r.Header.Get("Overwrite") + srcPath := path.Join(ns, r.URL.Path) - dst, err := extractDestination(dstHeader, r.Context().Value(ctxKeyBaseURI).(string)) + dstPath, err := extractDestination(r) if err != nil { w.WriteHeader(http.StatusBadRequest) return } - dst = path.Join(ns, dst) + dstPath = path.Join(ns, dstPath) - sublog := appctx.GetLogger(ctx).With().Str("src", src).Str("dst", dst).Logger() - sublog.Debug().Str("overwrite", overwrite).Msg("move") + sublog := appctx.GetLogger(ctx).With().Str("src", srcPath).Str("dst", dstPath).Logger() + src := &provider.Reference{Path: srcPath} + dst := &provider.Reference{Path: dstPath} + + intermediateDirRefFunc := func() (*provider.Reference, *rpc.Status, error) { + intermediateDir := path.Dir(dstPath) + ref := &provider.Reference{Path: intermediateDir} + return ref, &rpc.Status{Code: rpc.Code_CODE_OK}, nil + } + s.handleMove(ctx, w, r, src, dst, intermediateDirRefFunc, sublog) +} + +func (s *svc) handleMove(ctx context.Context, w http.ResponseWriter, r *http.Request, src, dst *provider.Reference, intermediateDirRef intermediateDirRefFunc, log zerolog.Logger) { + overwrite := r.Header.Get(HeaderOverwrite) + log.Debug().Str("overwrite", overwrite).Msg("move") overwrite = strings.ToUpper(overwrite) if overwrite == "" { @@ -61,18 +74,16 @@ func (s *svc) handleMove(w http.ResponseWriter, r *http.Request, ns string) { client, err := s.getClient() if err != nil { - sublog.Error().Err(err).Msg("error getting grpc client") + log.Error().Err(err).Msg("error getting grpc client") w.WriteHeader(http.StatusInternalServerError) return } // check src exists - srcStatReq := &provider.StatRequest{ - Ref: &provider.Reference{Path: src}, - } + srcStatReq := &provider.StatRequest{Ref: src} srcStatRes, err := client.Stat(ctx, srcStatReq) if err != nil { - sublog.Error().Err(err).Msg("error sending grpc stat request") + log.Error().Err(err).Msg("error sending grpc stat request") w.WriteHeader(http.StatusInternalServerError) return } @@ -84,23 +95,22 @@ func (s *svc) handleMove(w http.ResponseWriter, r *http.Request, ns string) { code: SabredavNotFound, message: m, }) - HandleWebdavError(&sublog, w, b, err) + HandleWebdavError(&log, w, b, err) } - HandleErrorStatus(&sublog, w, srcStatRes.Status) + HandleErrorStatus(&log, w, srcStatRes.Status) return } // check dst exists - dstStatRef := &provider.Reference{Path: dst} - dstStatReq := &provider.StatRequest{Ref: dstStatRef} + dstStatReq := &provider.StatRequest{Ref: dst} dstStatRes, err := client.Stat(ctx, dstStatReq) if err != nil { - sublog.Error().Err(err).Msg("error getting grpc client") + log.Error().Err(err).Msg("error getting grpc client") w.WriteHeader(http.StatusInternalServerError) return } if dstStatRes.Status.Code != rpc.Code_CODE_OK && dstStatRes.Status.Code != rpc.Code_CODE_NOT_FOUND { - HandleErrorStatus(&sublog, w, srcStatRes.Status) + HandleErrorStatus(&log, w, srcStatRes.Status) return } @@ -109,54 +119,60 @@ func (s *svc) handleMove(w http.ResponseWriter, r *http.Request, ns string) { successCode = http.StatusNoContent // 204 if target already existed, see https://tools.ietf.org/html/rfc4918#section-9.9.4 if overwrite == "F" { - sublog.Warn().Str("overwrite", overwrite).Msg("dst already exists") + log.Warn().Str("overwrite", overwrite).Msg("dst already exists") w.WriteHeader(http.StatusPreconditionFailed) // 412, see https://tools.ietf.org/html/rfc4918#section-9.9.4 return } // delete existing tree - delReq := &provider.DeleteRequest{Ref: dstStatRef} + delReq := &provider.DeleteRequest{Ref: dst} delRes, err := client.Delete(ctx, delReq) if err != nil { - sublog.Error().Err(err).Msg("error sending grpc delete request") + log.Error().Err(err).Msg("error sending grpc delete request") w.WriteHeader(http.StatusInternalServerError) return } if delRes.Status.Code != rpc.Code_CODE_OK && delRes.Status.Code != rpc.Code_CODE_NOT_FOUND { - HandleErrorStatus(&sublog, w, delRes.Status) + HandleErrorStatus(&log, w, delRes.Status) return } } else { // check if an intermediate path / the parent exists - intermediateDir := path.Dir(dst) - ref2 := &provider.Reference{Path: intermediateDir} - intStatReq := &provider.StatRequest{Ref: ref2} + dst, status, err := intermediateDirRef() + if err != nil { + log.Error().Err(err).Msg("error sending a grpc request") + w.WriteHeader(http.StatusInternalServerError) + return + } else if status.Code != rpc.Code_CODE_OK { + HandleErrorStatus(&log, w, status) + return + } + + intStatReq := &provider.StatRequest{Ref: dst} intStatRes, err := client.Stat(ctx, intStatReq) if err != nil { - sublog.Error().Err(err).Msg("error sending grpc stat request") + log.Error().Err(err).Msg("error sending grpc stat request") w.WriteHeader(http.StatusInternalServerError) return } if intStatRes.Status.Code != rpc.Code_CODE_OK { if intStatRes.Status.Code == rpc.Code_CODE_NOT_FOUND { // 409 if intermediate dir is missing, see https://tools.ietf.org/html/rfc4918#section-9.8.5 - sublog.Debug().Str("parent", intermediateDir).Interface("status", intStatRes.Status).Msg("conflict") + log.Debug().Interface("parent", dst).Interface("status", intStatRes.Status).Msg("conflict") w.WriteHeader(http.StatusConflict) } else { - HandleErrorStatus(&sublog, w, intStatRes.Status) + HandleErrorStatus(&log, w, intStatRes.Status) } return } // TODO what if intermediate is a file? } - sourceRef := &provider.Reference{Path: src} - dstRef := &provider.Reference{Path: dst} - mReq := &provider.MoveRequest{Source: sourceRef, Destination: dstRef} + mReq := &provider.MoveRequest{Source: src, Destination: dst} mRes, err := client.Move(ctx, mReq) if err != nil { - sublog.Error().Err(err).Msg("error sending move grpc request") + log.Error().Err(err).Msg("error sending move grpc request") w.WriteHeader(http.StatusInternalServerError) return } @@ -164,33 +180,33 @@ func (s *svc) handleMove(w http.ResponseWriter, r *http.Request, ns string) { if mRes.Status.Code != rpc.Code_CODE_OK { if mRes.Status.Code == rpc.Code_CODE_PERMISSION_DENIED { w.WriteHeader(http.StatusForbidden) - m := fmt.Sprintf("Permission denied to move %v", sourceRef.Path) + m := fmt.Sprintf("Permission denied to move %v", src.Path) b, err := Marshal(exception{ code: SabredavPermissionDenied, message: m, }) - HandleWebdavError(&sublog, w, b, err) + HandleWebdavError(&log, w, b, err) } - HandleErrorStatus(&sublog, w, mRes.Status) + HandleErrorStatus(&log, w, mRes.Status) return } dstStatRes, err = client.Stat(ctx, dstStatReq) if err != nil { - sublog.Error().Err(err).Msg("error sending grpc stat request") + log.Error().Err(err).Msg("error sending grpc stat request") w.WriteHeader(http.StatusInternalServerError) return } if dstStatRes.Status.Code != rpc.Code_CODE_OK { - HandleErrorStatus(&sublog, w, dstStatRes.Status) + HandleErrorStatus(&log, w, dstStatRes.Status) return } info := dstStatRes.Info - w.Header().Set("Content-Type", info.MimeType) - w.Header().Set("ETag", info.Etag) - w.Header().Set("OC-FileId", wrapResourceID(info.Id)) - w.Header().Set("OC-ETag", info.Etag) + w.Header().Set(HeaderContentType, info.MimeType) + w.Header().Set(HeaderETag, info.Etag) + w.Header().Set(HeaderOCFileID, wrapResourceID(info.Id)) + w.Header().Set(HeaderOCETag, info.Etag) w.WriteHeader(successCode) } diff --git a/internal/http/services/owncloud/ocdav/ocdav.go b/internal/http/services/owncloud/ocdav/ocdav.go index c2b7561cf2..93134dd96d 100644 --- a/internal/http/services/owncloud/ocdav/ocdav.go +++ b/internal/http/services/owncloud/ocdav/ocdav.go @@ -52,6 +52,10 @@ const ( ctxKeyBaseURI ctxKey = iota ) +var ( + errInvalidValue = errors.New("invalid value") +) + func init() { global.Register("ocdav", New) } @@ -263,20 +267,22 @@ func addAccessHeaders(w http.ResponseWriter, r *http.Request) { } } -func extractDestination(dstHeader, baseURI string) (string, error) { +func extractDestination(r *http.Request) (string, error) { + dstHeader := r.Header.Get(HeaderDestination) if dstHeader == "" { - return "", errors.New("destination header is empty") + return "", errors.Wrap(errInvalidValue, "destination header is empty") } dstURL, err := url.ParseRequestURI(dstHeader) if err != nil { return "", err } + baseURI := r.Context().Value(ctxKeyBaseURI).(string) // TODO check if path is on same storage, return 502 on problems, see https://tools.ietf.org/html/rfc4918#section-9.9.4 // Strip the base URI from the destination. The destination might contain redirection prefixes which need to be handled urlSplit := strings.Split(dstURL.Path, baseURI) if len(urlSplit) != 2 { - return "", errors.New("destination path does not contain base URI") + return "", errors.Wrap(errInvalidValue, "destination path does not contain base URI") } return urlSplit[1], nil diff --git a/internal/http/services/owncloud/ocdav/propfind.go b/internal/http/services/owncloud/ocdav/propfind.go index 9e71d6b036..6b2a9e8fd2 100644 --- a/internal/http/services/owncloud/ocdav/propfind.go +++ b/internal/http/services/owncloud/ocdav/propfind.go @@ -43,6 +43,7 @@ import ( "github.com/cs3org/reva/pkg/appctx" ctxuser "github.com/cs3org/reva/pkg/user" "github.com/cs3org/reva/pkg/utils" + "github.com/rs/zerolog" ) const ( @@ -61,26 +62,15 @@ const ( ) // ns is the namespace that is prefixed to the path in the cs3 namespace -func (s *svc) handlePropfind(w http.ResponseWriter, r *http.Request, ns string) { +func (s *svc) handlePathPropfind(w http.ResponseWriter, r *http.Request, ns string) { ctx := r.Context() ctx, span := trace.StartSpan(ctx, "propfind") defer span.End() fn := path.Join(ns, r.URL.Path) - depth := r.Header.Get("Depth") - if depth == "" { - depth = "1" - } sublog := appctx.GetLogger(ctx).With().Str("path", fn).Logger() - // see https://tools.ietf.org/html/rfc4918#section-9.1 - if depth != "0" && depth != "1" && depth != "infinity" { - sublog.Debug().Str("depth", depth).Msgf("invalid Depth header value") - w.WriteHeader(http.StatusBadRequest) - return - } - pf, status, err := readPropfind(r.Body) if err != nil { sublog.Debug().Err(err).Msg("error reading propfind request") @@ -88,14 +78,66 @@ func (s *svc) handlePropfind(w http.ResponseWriter, r *http.Request, ns string) return } - client, err := s.getClient() + ref := &provider.Reference{Path: fn} + + parentInfo, resourceInfos, ok := s.getResourceInfos(ctx, w, r, pf, ref, sublog) + if !ok { + // getResourceInfos handles responses in case of an error so we can just return here. + return + } + s.propfindResponse(ctx, w, r, ns, pf, parentInfo, resourceInfos, sublog) +} + +func (s *svc) propfindResponse(ctx context.Context, w http.ResponseWriter, r *http.Request, namespace string, pf propfindXML, parentInfo *provider.ResourceInfo, resourceInfos []*provider.ResourceInfo, log zerolog.Logger) { + propRes, err := s.formatPropfind(ctx, &pf, resourceInfos, namespace) if err != nil { - sublog.Error().Err(err).Msg("error getting grpc client") + log.Error().Err(err).Msg("error formatting propfind") w.WriteHeader(http.StatusInternalServerError) return } + w.Header().Set(HeaderDav, "1, 3, extended-mkcol") + w.Header().Set(HeaderContentType, "application/xml; charset=utf-8") + + var disableTus bool + // let clients know this collection supports tus.io POST requests to start uploads + if parentInfo.Type == provider.ResourceType_RESOURCE_TYPE_CONTAINER { + if parentInfo.Opaque != nil { + _, disableTus = parentInfo.Opaque.Map["disable_tus"] + } + if !disableTus { + 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.WriteHeader(http.StatusMultiStatus) + if _, err := w.Write([]byte(propRes)); err != nil { + log.Err(err).Msg("error writing response") + } +} + +func (s *svc) getResourceInfos(ctx context.Context, w http.ResponseWriter, r *http.Request, pf propfindXML, ref *provider.Reference, log zerolog.Logger) (*provider.ResourceInfo, []*provider.ResourceInfo, bool) { + depth := r.Header.Get(HeaderDepth) + if depth == "" { + depth = "1" + } + // see https://tools.ietf.org/html/rfc4918#section-9.1 + if depth != "0" && depth != "1" && depth != "infinity" { + log.Debug().Str("depth", depth).Msgf("invalid Depth header value") + w.WriteHeader(http.StatusBadRequest) + return nil, nil, false + } + + client, err := s.getClient() + if err != nil { + log.Error().Err(err).Msg("error getting grpc client") + w.WriteHeader(http.StatusInternalServerError) + return nil, nil, false + } + + var metadataKeys []string - metadataKeys := []string{} if pf.Allprop != nil { // TODO this changes the behavior and returns all properties if allprops has been set, // but allprops should only return some default properties @@ -110,46 +152,43 @@ func (s *svc) handlePropfind(w http.ResponseWriter, r *http.Request, ns string) } } } - ref := &provider.Reference{Path: fn} req := &provider.StatRequest{ Ref: ref, ArbitraryMetadataKeys: metadataKeys, } res, err := client.Stat(ctx, req) if err != nil { - sublog.Error().Err(err).Interface("req", req).Msg("error sending a grpc stat request") + log.Error().Err(err).Interface("req", req).Msg("error sending a grpc stat request") w.WriteHeader(http.StatusInternalServerError) - return - } - - if res.Status.Code != rpc.Code_CODE_OK { - HandleErrorStatus(&sublog, w, res.Status) - return + return nil, nil, false + } else if res.Status.Code != rpc.Code_CODE_OK { + HandleErrorStatus(&log, w, res.Status) + return nil, nil, false } - info := res.Info - infos := []*provider.ResourceInfo{info} - if info.Type == provider.ResourceType_RESOURCE_TYPE_CONTAINER && depth == "1" { + parentInfo := res.Info + resourceInfos := []*provider.ResourceInfo{parentInfo} + if parentInfo.Type == provider.ResourceType_RESOURCE_TYPE_CONTAINER && depth == "1" { req := &provider.ListContainerRequest{ Ref: ref, ArbitraryMetadataKeys: metadataKeys, } res, err := client.ListContainer(ctx, req) if err != nil { - sublog.Error().Err(err).Msg("error sending list container grpc request") + log.Error().Err(err).Msg("error sending list container grpc request") w.WriteHeader(http.StatusInternalServerError) - return + return nil, nil, false } if res.Status.Code != rpc.Code_CODE_OK { - HandleErrorStatus(&sublog, w, res.Status) - return + HandleErrorStatus(&log, w, res.Status) + return nil, nil, false } - infos = append(infos, res.Infos...) + resourceInfos = append(resourceInfos, res.Infos...) } else if depth == "infinity" { // FIXME: doesn't work cross-storage as the results will have the wrong paths! // use a stack to explore sub-containers breadth-first - stack := []string{info.Path} + stack := []string{parentInfo.Path} for len(stack) > 0 { // retrieve path on top of stack path := stack[len(stack)-1] @@ -160,16 +199,16 @@ func (s *svc) handlePropfind(w http.ResponseWriter, r *http.Request, ns string) } res, err := client.ListContainer(ctx, req) if err != nil { - sublog.Error().Err(err).Str("path", path).Msg("error sending list container grpc request") + log.Error().Err(err).Str("path", path).Msg("error sending list container grpc request") w.WriteHeader(http.StatusInternalServerError) - return + return nil, nil, false } if res.Status.Code != rpc.Code_CODE_OK { - HandleErrorStatus(&sublog, w, res.Status) - return + HandleErrorStatus(&log, w, res.Status) + return nil, nil, false } - infos = append(infos, res.Infos...) + resourceInfos = append(resourceInfos, res.Infos...) if depth != "infinity" { break @@ -190,32 +229,7 @@ func (s *svc) handlePropfind(w http.ResponseWriter, r *http.Request, ns string) } } - propRes, err := s.formatPropfind(ctx, &pf, infos, ns) - if err != nil { - sublog.Error().Err(err).Msg("error formatting propfind") - w.WriteHeader(http.StatusInternalServerError) - return - } - w.Header().Set("DAV", "1, 3, extended-mkcol") - w.Header().Set("Content-Type", "application/xml; charset=utf-8") - - var disableTus bool - // let clients know this collection supports tus.io POST requests to start uploads - if info.Type == provider.ResourceType_RESOURCE_TYPE_CONTAINER { - if info.Opaque != nil { - _, disableTus = info.Opaque.Map["disable_tus"] - } - if !disableTus { - w.Header().Add("Access-Control-Expose-Headers", "Tus-Resumable, Tus-Version, Tus-Extension") - w.Header().Set("Tus-Resumable", "1.0.0") - w.Header().Set("Tus-Version", "1.0.0") - w.Header().Set("Tus-Extension", "creation,creation-with-upload") - } - } - w.WriteHeader(http.StatusMultiStatus) - if _, err := w.Write([]byte(propRes)); err != nil { - sublog.Err(err).Msg("error writing response") - } + return parentInfo, resourceInfos, true } func requiresExplicitFetching(n *xml.Name) bool { diff --git a/internal/http/services/owncloud/ocdav/proppatch.go b/internal/http/services/owncloud/ocdav/proppatch.go index 0123aa7634..9b7fcbb459 100644 --- a/internal/http/services/owncloud/ocdav/proppatch.go +++ b/internal/http/services/owncloud/ocdav/proppatch.go @@ -33,16 +33,14 @@ import ( provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" "github.com/cs3org/reva/pkg/appctx" "github.com/pkg/errors" + "github.com/rs/zerolog" ) -func (s *svc) handleProppatch(w http.ResponseWriter, r *http.Request, ns string) { +func (s *svc) handlePathProppatch(w http.ResponseWriter, r *http.Request, ns string) { ctx := r.Context() ctx, span := trace.StartSpan(ctx, "proppatch") defer span.End() - acceptedProps := []xml.Name{} - removedProps := []xml.Name{} - fn := path.Join(ns, r.URL.Path) sublog := appctx.GetLogger(ctx).With().Str("path", fn).Logger() @@ -67,10 +65,9 @@ func (s *svc) handleProppatch(w http.ResponseWriter, r *http.Request, ns string) return } + ref := &provider.Reference{Path: fn} // check if resource exists - statReq := &provider.StatRequest{ - Ref: &provider.Reference{Path: fn}, - } + statReq := &provider.StatRequest{Ref: ref} statRes, err := c.Stat(ctx, statReq) if err != nil { sublog.Error().Err(err).Msg("error sending a grpc stat request") @@ -92,26 +89,53 @@ func (s *svc) handleProppatch(w http.ResponseWriter, r *http.Request, ns string) return } + acceptedProps, removedProps, ok := s.handleProppatch(ctx, w, r, ref, pp, sublog) + if !ok { + // handleProppatch handles responses in error cases so we can just return + return + } + + nRef := strings.TrimPrefix(fn, ns) + nRef = path.Join(ctx.Value(ctxKeyBaseURI).(string), nRef) + if statRes.Info.Type == provider.ResourceType_RESOURCE_TYPE_CONTAINER { + nRef += "/" + } + + s.handleProppatchResponse(ctx, w, r, acceptedProps, removedProps, nRef, sublog) +} + +func (s *svc) handleProppatch(ctx context.Context, w http.ResponseWriter, r *http.Request, ref *provider.Reference, patches []Proppatch, log zerolog.Logger) ([]xml.Name, []xml.Name, bool) { + c, err := s.getClient() + if err != nil { + log.Error().Err(err).Msg("error getting grpc client") + w.WriteHeader(http.StatusInternalServerError) + return nil, nil, false + } + rreq := &provider.UnsetArbitraryMetadataRequest{ - Ref: &provider.Reference{Path: fn}, + Ref: ref, ArbitraryMetadataKeys: []string{""}, } sreq := &provider.SetArbitraryMetadataRequest{ - Ref: &provider.Reference{Path: fn}, + Ref: ref, ArbitraryMetadata: &provider.ArbitraryMetadata{ Metadata: map[string]string{}, }, } - for i := range pp { - if len(pp[i].Props) < 1 { + + acceptedProps := []xml.Name{} + removedProps := []xml.Name{} + + for i := range patches { + if len(patches[i].Props) < 1 { continue } - for j := range pp[i].Props { - propNameXML := pp[i].Props[j].XMLName + for j := range patches[i].Props { + propNameXML := patches[i].Props[j].XMLName // don't use path.Join. It removes the double slash! concatenate with a / - key := fmt.Sprintf("%s/%s", pp[i].Props[j].XMLName.Space, pp[i].Props[j].XMLName.Local) - value := string(pp[i].Props[j].InnerXML) - remove := pp[i].Remove + key := fmt.Sprintf("%s/%s", patches[i].Props[j].XMLName.Space, patches[i].Props[j].XMLName.Local) + value := string(patches[i].Props[j].InnerXML) + remove := patches[i].Remove // boolean flags may be "set" to false as well if s.isBooleanProperty(key) { // Make boolean properties either "0" or "1" @@ -128,48 +152,48 @@ func (s *svc) handleProppatch(w http.ResponseWriter, r *http.Request, ns string) rreq.ArbitraryMetadataKeys[0] = key res, err := c.UnsetArbitraryMetadata(ctx, rreq) if err != nil { - sublog.Error().Err(err).Msg("error sending a grpc UnsetArbitraryMetadata request") + log.Error().Err(err).Msg("error sending a grpc UnsetArbitraryMetadata request") w.WriteHeader(http.StatusInternalServerError) - return + return nil, nil, false } if res.Status.Code != rpc.Code_CODE_OK { if res.Status.Code == rpc.Code_CODE_PERMISSION_DENIED { w.WriteHeader(http.StatusForbidden) - m := fmt.Sprintf("Permission denied to remove properties on resource %v", fn) + m := fmt.Sprintf("Permission denied to remove properties on resource %v", ref.Path) b, err := Marshal(exception{ code: SabredavPermissionDenied, message: m, }) - HandleWebdavError(&sublog, w, b, err) - return + HandleWebdavError(&log, w, b, err) + return nil, nil, false } - HandleErrorStatus(&sublog, w, res.Status) - return + HandleErrorStatus(&log, w, res.Status) + return nil, nil, false } removedProps = append(removedProps, propNameXML) } else { sreq.ArbitraryMetadata.Metadata[key] = value res, err := c.SetArbitraryMetadata(ctx, sreq) if err != nil { - sublog.Error().Err(err).Str("key", key).Str("value", value).Msg("error sending a grpc SetArbitraryMetadata request") + log.Error().Err(err).Str("key", key).Str("value", value).Msg("error sending a grpc SetArbitraryMetadata request") w.WriteHeader(http.StatusInternalServerError) - return + return nil, nil, false } if res.Status.Code != rpc.Code_CODE_OK { if res.Status.Code == rpc.Code_CODE_PERMISSION_DENIED { w.WriteHeader(http.StatusForbidden) - m := fmt.Sprintf("Permission denied to set properties on resource %v", fn) + m := fmt.Sprintf("Permission denied to set properties on resource %v", ref.Path) b, err := Marshal(exception{ code: SabredavPermissionDenied, message: m, }) - HandleWebdavError(&sublog, w, b, err) - return + HandleWebdavError(&log, w, b, err) + return nil, nil, false } - HandleErrorStatus(&sublog, w, res.Status) - return + HandleErrorStatus(&log, w, res.Status) + return nil, nil, false } acceptedProps = append(acceptedProps, propNameXML) @@ -181,23 +205,21 @@ func (s *svc) handleProppatch(w http.ResponseWriter, r *http.Request, ns string) // http://www.webdav.org/specs/rfc2518.html#rfc.section.8.2 } - ref := strings.TrimPrefix(fn, ns) - ref = path.Join(ctx.Value(ctxKeyBaseURI).(string), ref) - if statRes.Info.Type == provider.ResourceType_RESOURCE_TYPE_CONTAINER { - ref += "/" - } + return acceptedProps, removedProps, true +} - propRes, err := s.formatProppatchResponse(ctx, acceptedProps, removedProps, ref) +func (s *svc) handleProppatchResponse(ctx context.Context, w http.ResponseWriter, r *http.Request, acceptedProps, removedProps []xml.Name, path string, log zerolog.Logger) { + propRes, err := s.formatProppatchResponse(ctx, acceptedProps, removedProps, path) if err != nil { - sublog.Error().Err(err).Msg("error formatting proppatch response") + log.Error().Err(err).Msg("error formatting proppatch response") w.WriteHeader(http.StatusInternalServerError) return } - w.Header().Set("DAV", "1, 3, extended-mkcol") - w.Header().Set("Content-Type", "application/xml; charset=utf-8") + w.Header().Set(HeaderDav, "1, 3, extended-mkcol") + w.Header().Set(HeaderContentType, "application/xml; charset=utf-8") w.WriteHeader(http.StatusMultiStatus) if _, err := w.Write([]byte(propRes)); err != nil { - sublog.Err(err).Msg("error writing response") + log.Err(err).Msg("error writing response") } } diff --git a/internal/http/services/owncloud/ocdav/publicfile.go b/internal/http/services/owncloud/ocdav/publicfile.go index c5670014f0..3d06da037d 100644 --- a/internal/http/services/owncloud/ocdav/publicfile.go +++ b/internal/http/services/owncloud/ocdav/publicfile.go @@ -51,34 +51,34 @@ func (h *PublicFileHandler) Handler(s *svc) http.Handler { if relativePath != "" && relativePath != "/" { // accessing the file // PROPFIND has an implicit call - if r.Method != "PROPFIND" && !s.adjustResourcePathInURL(w, r) { + if r.Method != MethodPropfind && !s.adjustResourcePathInURL(w, r) { return } r.URL.Path = path.Base(r.URL.Path) switch r.Method { - case "PROPFIND": + case MethodPropfind: s.handlePropfindOnToken(w, r, h.namespace, false) case http.MethodGet: - s.handleGet(w, r, h.namespace) + s.handlePathGet(w, r, h.namespace) case http.MethodOptions: s.handleOptions(w, r, h.namespace) case http.MethodHead: - s.handleHead(w, r, h.namespace) + s.handlePathHead(w, r, h.namespace) case http.MethodPut: - s.handlePut(w, r, h.namespace) + s.handlePathPut(w, r, h.namespace) default: w.WriteHeader(http.StatusMethodNotAllowed) } } else { // accessing the virtual parent folder switch r.Method { - case "PROPFIND": + case MethodPropfind: s.handlePropfindOnToken(w, r, h.namespace, true) case http.MethodOptions: s.handleOptions(w, r, h.namespace) case http.MethodHead: - s.handleHead(w, r, h.namespace) + s.handlePathHead(w, r, h.namespace) default: w.WriteHeader(http.StatusMethodNotAllowed) } @@ -138,7 +138,7 @@ func (s *svc) handlePropfindOnToken(w http.ResponseWriter, r *http.Request, ns s sublog := appctx.GetLogger(ctx).With().Interface("tokenStatInfo", tokenStatInfo).Logger() sublog.Debug().Msg("handlePropfindOnToken") - depth := r.Header.Get("Depth") + depth := r.Header.Get(HeaderDepth) if depth == "" { depth = "1" } @@ -195,8 +195,8 @@ func (s *svc) handlePropfindOnToken(w http.ResponseWriter, r *http.Request, ns s return } - w.Header().Set("DAV", "1, 3, extended-mkcol") - w.Header().Set("Content-Type", "application/xml; charset=utf-8") + w.Header().Set(HeaderDav, "1, 3, extended-mkcol") + w.Header().Set(HeaderContentType, "application/xml; charset=utf-8") w.WriteHeader(http.StatusMultiStatus) if _, err := w.Write([]byte(propRes)); err != nil { sublog.Err(err).Msg("error writing response") diff --git a/internal/http/services/owncloud/ocdav/put.go b/internal/http/services/owncloud/ocdav/put.go index f767adfa8e..3873f74826 100644 --- a/internal/http/services/owncloud/ocdav/put.go +++ b/internal/http/services/owncloud/ocdav/put.go @@ -19,7 +19,7 @@ package ocdav import ( - "io" + "context" "net/http" "path" "strconv" @@ -35,11 +35,11 @@ import ( "github.com/cs3org/reva/pkg/rhttp" "github.com/cs3org/reva/pkg/storage/utils/chunking" "github.com/cs3org/reva/pkg/utils" - "go.opencensus.io/trace" + "github.com/rs/zerolog" ) func sufferMacOSFinder(r *http.Request) bool { - return r.Header.Get("X-Expected-Entity-Length") != "" + return r.Header.Get(HeaderExpectedEntityLength) != "" } func handleMacOSFinder(w http.ResponseWriter, r *http.Request) error { @@ -61,8 +61,8 @@ func handleMacOSFinder(w http.ResponseWriter, r *http.Request) error { */ log := appctx.GetLogger(r.Context()) - content := r.Header.Get("Content-Length") - expected := r.Header.Get("X-Expected-Entity-Length") + content := r.Header.Get(HeaderContentLength) + expected := r.Header.Get(HeaderExpectedEntityLength) log.Warn().Str("content-length", content).Str("x-expected-entity-length", expected).Msg("Mac OS Finder corner-case detected") // The best mitigation to this problem is to tell users to not use crappy Finder. @@ -100,87 +100,63 @@ func isContentRange(r *http.Request) bool { in unexpected behaviour (cf PEAR::HTTP_WebDAV_Client 1.0.1), we reject all PUT requests with a Content-Range for now. */ - return r.Header.Get("Content-Range") != "" + return r.Header.Get(HeaderContentRange) != "" } -func (s *svc) handlePut(w http.ResponseWriter, r *http.Request, ns string) { +func (s *svc) handlePathPut(w http.ResponseWriter, r *http.Request, ns string) { ctx := r.Context() fn := path.Join(ns, r.URL.Path) sublog := appctx.GetLogger(ctx).With().Str("path", fn).Logger() - if r.Body == nil { - sublog.Debug().Msg("body is nil") - w.WriteHeader(http.StatusBadRequest) - return - } + ref := &provider.Reference{Path: fn} - if isContentRange(r) { - sublog.Debug().Msg("Content-Range not supported for PUT") - w.WriteHeader(http.StatusNotImplemented) - return - } + s.handlePut(ctx, w, r, ref, sublog) +} - if sufferMacOSFinder(r) { - err := handleMacOSFinder(w, r) - if err != nil { - sublog.Debug().Err(err).Msg("error handling Mac OS corner-case") - w.WriteHeader(http.StatusInternalServerError) - return - } +func (s *svc) handlePut(ctx context.Context, w http.ResponseWriter, r *http.Request, ref *provider.Reference, log zerolog.Logger) { + if !checkPreconditions(w, r, log) { + // checkPreconditions handles error returns + return } - length, err := strconv.ParseInt(r.Header.Get("Content-Length"), 10, 64) + length, err := getContentLength(w, r) if err != nil { - // Fallback to Upload-Length - length, err = strconv.ParseInt(r.Header.Get("Upload-Length"), 10, 64) - if err != nil { - w.WriteHeader(http.StatusBadRequest) - return - } + w.WriteHeader(http.StatusBadRequest) + return } - s.handlePutHelper(w, r, r.Body, fn, length) -} - -func (s *svc) handlePutHelper(w http.ResponseWriter, r *http.Request, content io.Reader, fn string, length int64) { - ctx := r.Context() - ctx, span := trace.StartSpan(ctx, "put") - defer span.End() - - sublog := appctx.GetLogger(ctx).With().Str("path", fn).Logger() client, err := s.getClient() if err != nil { - sublog.Error().Err(err).Msg("error getting grpc client") + log.Error().Err(err).Msg("error getting grpc client") w.WriteHeader(http.StatusInternalServerError) return } - ref := &provider.Reference{Path: fn} sReq := &provider.StatRequest{Ref: ref} sRes, err := client.Stat(ctx, sReq) if err != nil { - sublog.Error().Err(err).Msg("error sending grpc stat request") + log.Error().Err(err).Msg("error sending grpc stat request") w.WriteHeader(http.StatusInternalServerError) return } if sRes.Status.Code != rpc.Code_CODE_OK && sRes.Status.Code != rpc.Code_CODE_NOT_FOUND { - HandleErrorStatus(&sublog, w, sRes.Status) + HandleErrorStatus(&log, w, sRes.Status) return } info := sRes.Info if info != nil { if info.Type != provider.ResourceType_RESOURCE_TYPE_FILE { - sublog.Debug().Msg("resource is not a file") + log.Debug().Msg("resource is not a file") w.WriteHeader(http.StatusConflict) return } - clientETag := r.Header.Get("If-Match") + clientETag := r.Header.Get(HeaderIfMatch) serverETag := info.Etag if clientETag != "" { if clientETag != serverETag { - sublog.Debug().Str("client-etag", clientETag).Str("server-etag", serverETag).Msg("etags mismatch") + log.Debug().Str("client-etag", clientETag).Str("server-etag", serverETag).Msg("etags mismatch") w.WriteHeader(http.StatusPreconditionFailed) return } @@ -188,38 +164,38 @@ func (s *svc) handlePutHelper(w http.ResponseWriter, r *http.Request, content io } opaqueMap := map[string]*typespb.OpaqueEntry{ - "Upload-Length": { + HeaderUploadLength: { Decoder: "plain", Value: []byte(strconv.FormatInt(length, 10)), }, } - if mtime := r.Header.Get("X-OC-Mtime"); mtime != "" { - opaqueMap["X-OC-Mtime"] = &typespb.OpaqueEntry{ + if mtime := r.Header.Get(HeaderOCMtime); mtime != "" { + opaqueMap[HeaderOCMtime] = &typespb.OpaqueEntry{ Decoder: "plain", Value: []byte(mtime), } // TODO: find a way to check if the storage really accepted the value - w.Header().Set("X-OC-Mtime", "accepted") + w.Header().Set(HeaderOCMtime, "accepted") } // curl -X PUT https://demo.owncloud.com/remote.php/webdav/testcs.bin -u demo:demo -d '123' -v -H 'OC-Checksum: SHA1:40bd001563085fc35165329ea1ff5c5ecbdbbeef' var cparts []string // TUS Upload-Checksum header takes precedence - if checksum := r.Header.Get("Upload-Checksum"); checksum != "" { + if checksum := r.Header.Get(HeaderUploadChecksum); checksum != "" { cparts = strings.SplitN(checksum, " ", 2) if len(cparts) != 2 { - sublog.Debug().Str("upload-checksum", checksum).Msg("invalid Upload-Checksum format, expected '[algorithm] [checksum]'") + log.Debug().Str("upload-checksum", checksum).Msg("invalid Upload-Checksum format, expected '[algorithm] [checksum]'") w.WriteHeader(http.StatusBadRequest) return } // Then try owncloud header - } else if checksum := r.Header.Get("OC-Checksum"); checksum != "" { + } else if checksum := r.Header.Get(HeaderOCChecksum); checksum != "" { cparts = strings.SplitN(checksum, ":", 2) if len(cparts) != 2 { - sublog.Debug().Str("oc-checksum", checksum).Msg("invalid OC-Checksum format, expected '[algorithm]:[checksum]'") + log.Debug().Str("oc-checksum", checksum).Msg("invalid OC-Checksum format, expected '[algorithm]:[checksum]'") w.WriteHeader(http.StatusBadRequest) return } @@ -227,7 +203,7 @@ func (s *svc) handlePutHelper(w http.ResponseWriter, r *http.Request, content io // we do not check the algorithm here, because it might depend on the storage if len(cparts) == 2 { // Translate into TUS style Upload-Checksum header - opaqueMap["Upload-Checksum"] = &typespb.OpaqueEntry{ + opaqueMap[HeaderUploadChecksum] = &typespb.OpaqueEntry{ Decoder: "plain", // algorithm is always lowercase, checksum is separated by space Value: []byte(strings.ToLower(cparts[0]) + " " + cparts[1]), @@ -242,7 +218,7 @@ func (s *svc) handlePutHelper(w http.ResponseWriter, r *http.Request, content io // where to upload the file? uRes, err := client.InitiateFileUpload(ctx, uReq) if err != nil { - sublog.Error().Err(err).Msg("error initiating file upload") + log.Error().Err(err).Msg("error initiating file upload") w.WriteHeader(http.StatusInternalServerError) return } @@ -254,9 +230,9 @@ func (s *svc) handlePutHelper(w http.ResponseWriter, r *http.Request, content io code: SabredavPermissionDenied, message: "permission denied: you have no permission to upload content", }) - HandleWebdavError(&sublog, w, b, err) + HandleWebdavError(&log, w, b, err) } - HandleErrorStatus(&sublog, w, uRes.Status) + HandleErrorStatus(&log, w, uRes.Status) return } @@ -268,7 +244,7 @@ func (s *svc) handlePutHelper(w http.ResponseWriter, r *http.Request, content io } if length > 0 { - httpReq, err := rhttp.NewRequest(ctx, "PUT", ep, content) + httpReq, err := rhttp.NewRequest(ctx, http.MethodPut, ep, r.Body) if err != nil { w.WriteHeader(http.StatusInternalServerError) return @@ -277,7 +253,7 @@ func (s *svc) handlePutHelper(w http.ResponseWriter, r *http.Request, content io httpRes, err := s.client.Do(httpReq) if err != nil { - sublog.Error().Err(err).Msg("error doing PUT request to data service") + log.Error().Err(err).Msg("error doing PUT request to data service") w.WriteHeader(http.StatusInternalServerError) return } @@ -293,21 +269,21 @@ func (s *svc) handlePutHelper(w http.ResponseWriter, r *http.Request, content io code: SabredavBadRequest, message: "The computed checksum does not match the one received from the client.", }) - HandleWebdavError(&sublog, w, b, err) + HandleWebdavError(&log, w, b, err) } - sublog.Error().Err(err).Msg("PUT request to data server failed") + log.Error().Err(err).Msg("PUT request to data server failed") w.WriteHeader(httpRes.StatusCode) return } } - ok, err := chunking.IsChunked(fn) + ok, err := chunking.IsChunked(ref.Path) if err != nil { w.WriteHeader(http.StatusInternalServerError) return } if ok { - chunk, err := chunking.GetChunkBLOBInfo(fn) + chunk, err := chunking.GetChunkBLOBInfo(ref.Path) if err != nil { w.WriteHeader(http.StatusInternalServerError) return @@ -318,25 +294,25 @@ func (s *svc) handlePutHelper(w http.ResponseWriter, r *http.Request, content io // stat again to check the new file's metadata sRes, err = client.Stat(ctx, sReq) if err != nil { - sublog.Error().Err(err).Msg("error sending grpc stat request") + log.Error().Err(err).Msg("error sending grpc stat request") w.WriteHeader(http.StatusInternalServerError) return } if sRes.Status.Code != rpc.Code_CODE_OK { - HandleErrorStatus(&sublog, w, sRes.Status) + HandleErrorStatus(&log, w, sRes.Status) return } newInfo := sRes.Info - w.Header().Add("Content-Type", newInfo.MimeType) - w.Header().Set("ETag", newInfo.Etag) - w.Header().Set("OC-FileId", wrapResourceID(newInfo.Id)) - w.Header().Set("OC-ETag", newInfo.Etag) + w.Header().Add(HeaderContentType, newInfo.MimeType) + w.Header().Set(HeaderETag, newInfo.Etag) + w.Header().Set(HeaderOCFileID, wrapResourceID(newInfo.Id)) + w.Header().Set(HeaderOCETag, newInfo.Etag) t := utils.TSToTime(newInfo.Mtime).UTC() lastModifiedString := t.Format(time.RFC1123Z) - w.Header().Set("Last-Modified", lastModifiedString) + w.Header().Set(HeaderLastModified, lastModifiedString) // file was new if info == nil { @@ -347,3 +323,33 @@ func (s *svc) handlePutHelper(w http.ResponseWriter, r *http.Request, content io // overwrite w.WriteHeader(http.StatusNoContent) } + +func checkPreconditions(w http.ResponseWriter, r *http.Request, log zerolog.Logger) bool { + if isContentRange(r) { + log.Debug().Msg("Content-Range not supported for PUT") + w.WriteHeader(http.StatusNotImplemented) + return false + } + + if sufferMacOSFinder(r) { + err := handleMacOSFinder(w, r) + if err != nil { + log.Debug().Err(err).Msg("error handling Mac OS corner-case") + w.WriteHeader(http.StatusInternalServerError) + return false + } + } + return true +} + +func getContentLength(w http.ResponseWriter, r *http.Request) (int64, error) { + length, err := strconv.ParseInt(r.Header.Get(HeaderContentLength), 10, 64) + if err != nil { + // Fallback to Upload-Length + length, err = strconv.ParseInt(r.Header.Get(HeaderUploadLength), 10, 64) + if err != nil { + return 0, err + } + } + return length, nil +} diff --git a/internal/http/services/owncloud/ocdav/trashbin.go b/internal/http/services/owncloud/ocdav/trashbin.go index e5eeb7db6c..a41b88ac47 100644 --- a/internal/http/services/owncloud/ocdav/trashbin.go +++ b/internal/http/services/owncloud/ocdav/trashbin.go @@ -106,11 +106,11 @@ func (h *TrashbinHandler) Handler(s *svc) http.Handler { // return //} - if r.Method == "PROPFIND" { + if r.Method == MethodPropfind { h.listTrashbin(w, r, s, u, key, r.URL.Path) return } - if key != "" && r.Method == "MOVE" { + if key != "" && r.Method == MethodMove { // find path in url relative to trash base trashBase := ctx.Value(ctxKeyBaseURI).(string) baseURI := path.Join(path.Dir(trashBase), "files", username) @@ -118,8 +118,7 @@ func (h *TrashbinHandler) Handler(s *svc) http.Handler { r = r.WithContext(ctx) // TODO make request.php optional in destination header - dstHeader := r.Header.Get("Destination") - dst, err := extractDestination(dstHeader, baseURI) + dst, err := extractDestination(r) if err != nil { w.WriteHeader(http.StatusBadRequest) return @@ -132,7 +131,7 @@ func (h *TrashbinHandler) Handler(s *svc) http.Handler { return } - if r.Method == "DELETE" { + if r.Method == http.MethodDelete { h.delete(w, r, s, u, key, r.URL.Path) return } @@ -146,7 +145,7 @@ func (h *TrashbinHandler) listTrashbin(w http.ResponseWriter, r *http.Request, s ctx, span := trace.StartSpan(ctx, "listTrashbin") defer span.End() - depth := r.Header.Get("Depth") + depth := r.Header.Get(HeaderDepth) if depth == "" { depth = "1" } @@ -248,8 +247,8 @@ func (h *TrashbinHandler) listTrashbin(w http.ResponseWriter, r *http.Request, s w.WriteHeader(http.StatusInternalServerError) return } - w.Header().Set("DAV", "1, 3, extended-mkcol") - w.Header().Set("Content-Type", "application/xml; charset=utf-8") + w.Header().Set(HeaderDav, "1, 3, extended-mkcol") + w.Header().Set(HeaderContentType, "application/xml; charset=utf-8") w.WriteHeader(http.StatusMultiStatus) _, err = w.Write([]byte(propRes)) if err != nil { @@ -420,7 +419,7 @@ func (h *TrashbinHandler) restore(w http.ResponseWriter, r *http.Request, s *svc sublog := appctx.GetLogger(ctx).With().Logger() - overwrite := r.Header.Get("Overwrite") + overwrite := r.Header.Get(HeaderOverwrite) overwrite = strings.ToUpper(overwrite) if overwrite == "" { @@ -480,7 +479,7 @@ func (h *TrashbinHandler) restore(w http.ResponseWriter, r *http.Request, s *svc b, err := Marshal(exception{ code: SabredavPreconditionFailed, message: "The destination node already exists, and the overwrite header is set to false", - header: "Overwrite", + header: HeaderOverwrite, }) HandleWebdavError(&sublog, w, b, err) return @@ -544,10 +543,10 @@ func (h *TrashbinHandler) restore(w http.ResponseWriter, r *http.Request, s *svc } info := dstStatRes.Info - w.Header().Set("Content-Type", info.MimeType) - w.Header().Set("ETag", info.Etag) - w.Header().Set("OC-FileId", wrapResourceID(info.Id)) - w.Header().Set("OC-ETag", info.Etag) + w.Header().Set(HeaderContentType, info.MimeType) + w.Header().Set(HeaderETag, info.Etag) + w.Header().Set(HeaderOCFileID, wrapResourceID(info.Id)) + w.Header().Set(HeaderOCETag, info.Etag) w.WriteHeader(successCode) } diff --git a/internal/http/services/owncloud/ocdav/tus.go b/internal/http/services/owncloud/ocdav/tus.go index 8fb4c7a1a1..91c07e7bec 100644 --- a/internal/http/services/owncloud/ocdav/tus.go +++ b/internal/http/services/owncloud/ocdav/tus.go @@ -19,6 +19,7 @@ package ocdav import ( + "context" "net/http" "path" "strconv" @@ -31,28 +32,47 @@ import ( "github.com/cs3org/reva/pkg/appctx" "github.com/cs3org/reva/pkg/rhttp" "github.com/cs3org/reva/pkg/utils" + "github.com/rs/zerolog" tusd "github.com/tus/tusd/pkg/handler" "go.opencensus.io/trace" ) -func (s *svc) handleTusPost(w http.ResponseWriter, r *http.Request, ns string) { +func (s *svc) handlePathTusPost(w http.ResponseWriter, r *http.Request, ns string) { ctx := r.Context() ctx, span := trace.StartSpan(ctx, "tus-post") defer span.End() - w.Header().Add("Access-Control-Allow-Headers", "Tus-Resumable, Upload-Length, Upload-Metadata, If-Match") - w.Header().Add("Access-Control-Expose-Headers", "Tus-Resumable, Location") + // read filename from metadata + meta := tusd.ParseMetadataHeader(r.Header.Get(HeaderUploadMetadata)) + if meta["filename"] == "" { + w.WriteHeader(http.StatusPreconditionFailed) + return + } + + // append filename to current dir + fn := path.Join(ns, r.URL.Path, meta["filename"]) + + sublog := appctx.GetLogger(ctx).With().Str("path", fn).Logger() + // check tus headers? + + ref := &provider.Reference{Path: fn} + s.handleTusPost(ctx, w, r, meta, ref, sublog) +} - w.Header().Set("Tus-Resumable", "1.0.0") +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(HeaderTusResumable, "1.0.0") // Test if the version sent by the client is supported // GET methods are not checked since a browser may visit this URL and does // not include this header. This request is not part of the specification. - if r.Header.Get("Tus-Resumable") != "1.0.0" { + if r.Header.Get(HeaderTusResumable) != "1.0.0" { w.WriteHeader(http.StatusPreconditionFailed) return } - if r.Header.Get("Upload-Length") == "" { + if r.Header.Get(HeaderUploadLength) == "" { w.WriteHeader(http.StatusPreconditionFailed) return } @@ -62,55 +82,42 @@ func (s *svc) handleTusPost(w http.ResponseWriter, r *http.Request, ns string) { // TODO check Expect: 100-continue - // read filename from metadata - meta := tusd.ParseMetadataHeader(r.Header.Get("Upload-Metadata")) - if meta["filename"] == "" { - w.WriteHeader(http.StatusPreconditionFailed) - return - } - - // append filename to current dir - fn := path.Join(ns, r.URL.Path, meta["filename"]) - - sublog := appctx.GetLogger(ctx).With().Str("path", fn).Logger() - // check tus headers? - // check if destination exists or is a file client, err := s.getClient() if err != nil { - sublog.Error().Err(err).Msg("error getting grpc client") + log.Error().Err(err).Msg("error getting grpc client") w.WriteHeader(http.StatusInternalServerError) return } sReq := &provider.StatRequest{ - Ref: &provider.Reference{Path: fn}, + Ref: ref, } sRes, err := client.Stat(ctx, sReq) if err != nil { - sublog.Error().Err(err).Msg("error sending grpc stat request") + log.Error().Err(err).Msg("error sending grpc stat request") w.WriteHeader(http.StatusInternalServerError) return } if sRes.Status.Code != rpc.Code_CODE_OK && sRes.Status.Code != rpc.Code_CODE_NOT_FOUND { - HandleErrorStatus(&sublog, w, sRes.Status) + HandleErrorStatus(&log, w, sRes.Status) return } info := sRes.Info if info != nil && info.Type != provider.ResourceType_RESOURCE_TYPE_FILE { - sublog.Warn().Msg("resource is not a file") + log.Warn().Msg("resource is not a file") w.WriteHeader(http.StatusConflict) return } if info != nil { - clientETag := r.Header.Get("If-Match") + clientETag := r.Header.Get(HeaderIfMatch) serverETag := info.Etag if clientETag != "" { if clientETag != serverETag { - sublog.Warn().Str("client-etag", clientETag).Str("server-etag", serverETag).Msg("etags mismatch") + log.Warn().Str("client-etag", clientETag).Str("server-etag", serverETag).Msg("etags mismatch") w.WriteHeader(http.StatusPreconditionFailed) return } @@ -118,15 +125,15 @@ func (s *svc) handleTusPost(w http.ResponseWriter, r *http.Request, ns string) { } opaqueMap := map[string]*typespb.OpaqueEntry{ - "Upload-Length": { + HeaderUploadLength: { Decoder: "plain", - Value: []byte(r.Header.Get("Upload-Length")), + Value: []byte(r.Header.Get(HeaderUploadLength)), }, } mtime := meta["mtime"] if mtime != "" { - opaqueMap["X-OC-Mtime"] = &typespb.OpaqueEntry{ + opaqueMap[HeaderOCMtime] = &typespb.OpaqueEntry{ Decoder: "plain", Value: []byte(mtime), } @@ -134,7 +141,7 @@ func (s *svc) handleTusPost(w http.ResponseWriter, r *http.Request, ns string) { // initiateUpload uReq := &provider.InitiateFileUploadRequest{ - Ref: &provider.Reference{Path: fn}, + Ref: ref, Opaque: &typespb.Opaque{ Map: opaqueMap, }, @@ -142,13 +149,13 @@ func (s *svc) handleTusPost(w http.ResponseWriter, r *http.Request, ns string) { uRes, err := client.InitiateFileUpload(ctx, uReq) if err != nil { - sublog.Error().Err(err).Msg("error initiating file upload") + log.Error().Err(err).Msg("error initiating file upload") w.WriteHeader(http.StatusInternalServerError) return } if uRes.Status.Code != rpc.Code_CODE_OK { - HandleErrorStatus(&sublog, w, uRes.Status) + HandleErrorStatus(&log, w, uRes.Status) return } @@ -168,15 +175,15 @@ func (s *svc) handleTusPost(w http.ResponseWriter, r *http.Request, ns string) { ep += token } - w.Header().Set("Location", ep) + w.Header().Set(HeaderLocation, ep) // for creation-with-upload extension forward bytes to dataprovider // TODO check this really streams - if r.Header.Get("Content-Type") == "application/offset+octet-stream" { + if r.Header.Get(HeaderContentType) == "application/offset+octet-stream" { - length, err := strconv.ParseInt(r.Header.Get("Content-Length"), 10, 64) + length, err := strconv.ParseInt(r.Header.Get(HeaderContentLength), 10, 64) if err != nil { - sublog.Debug().Err(err).Msg("wrong request") + log.Debug().Err(err).Msg("wrong request") w.WriteHeader(http.StatusBadRequest) return } @@ -184,73 +191,73 @@ func (s *svc) handleTusPost(w http.ResponseWriter, r *http.Request, ns string) { var httpRes *http.Response if length != 0 { - httpReq, err := rhttp.NewRequest(ctx, "PATCH", ep, r.Body) + httpReq, err := rhttp.NewRequest(ctx, http.MethodPatch, ep, r.Body) if err != nil { - sublog.Debug().Err(err).Msg("wrong request") + log.Debug().Err(err).Msg("wrong request") w.WriteHeader(http.StatusInternalServerError) return } - httpReq.Header.Set("Content-Type", r.Header.Get("Content-Type")) - httpReq.Header.Set("Content-Length", r.Header.Get("Content-Length")) - if r.Header.Get("Upload-Offset") != "" { - httpReq.Header.Set("Upload-Offset", r.Header.Get("Upload-Offset")) + httpReq.Header.Set(HeaderContentType, r.Header.Get(HeaderContentType)) + httpReq.Header.Set(HeaderContentLength, r.Header.Get(HeaderContentLength)) + if r.Header.Get(HeaderUploadOffset) != "" { + httpReq.Header.Set(HeaderUploadOffset, r.Header.Get(HeaderUploadOffset)) } else { - httpReq.Header.Set("Upload-Offset", "0") + httpReq.Header.Set(HeaderUploadOffset, "0") } - httpReq.Header.Set("Tus-Resumable", r.Header.Get("Tus-Resumable")) + httpReq.Header.Set(HeaderTusResumable, r.Header.Get(HeaderTusResumable)) httpRes, err = s.client.Do(httpReq) if err != nil { - sublog.Error().Err(err).Msg("error doing GET request to data service") + log.Error().Err(err).Msg("error doing GET request to data service") w.WriteHeader(http.StatusInternalServerError) return } defer httpRes.Body.Close() - w.Header().Set("Upload-Offset", httpRes.Header.Get("Upload-Offset")) - w.Header().Set("Tus-Resumable", httpRes.Header.Get("Tus-Resumable")) + w.Header().Set(HeaderUploadOffset, httpRes.Header.Get(HeaderUploadOffset)) + w.Header().Set(HeaderTusResumable, httpRes.Header.Get(HeaderTusResumable)) if httpRes.StatusCode != http.StatusNoContent { w.WriteHeader(httpRes.StatusCode) return } } else { - sublog.Debug().Msg("Skipping sending a Patch request as body is empty") + log.Debug().Msg("Skipping sending a Patch request as body is empty") } // check if upload was fully completed - if length == 0 || httpRes.Header.Get("Upload-Offset") == r.Header.Get("Upload-Length") { + if length == 0 || httpRes.Header.Get(HeaderUploadOffset) == r.Header.Get(HeaderUploadLength) { // get uploaded file metadata sRes, err := client.Stat(ctx, sReq) if err != nil { - sublog.Error().Err(err).Msg("error sending grpc stat request") + log.Error().Err(err).Msg("error sending grpc stat request") w.WriteHeader(http.StatusInternalServerError) return } if sRes.Status.Code != rpc.Code_CODE_OK && sRes.Status.Code != rpc.Code_CODE_NOT_FOUND { - HandleErrorStatus(&sublog, w, sRes.Status) + HandleErrorStatus(&log, w, sRes.Status) return } info := sRes.Info if info == nil { - sublog.Error().Msg("No info found for uploaded file") + log.Error().Msg("No info found for uploaded file") w.WriteHeader(http.StatusInternalServerError) return } - if httpRes != nil && httpRes.Header != nil && httpRes.Header.Get("X-OC-Mtime") != "" { + if httpRes != nil && httpRes.Header != nil && httpRes.Header.Get(HeaderOCMtime) != "" { // set the "accepted" value if returned in the upload response headers - w.Header().Set("X-OC-Mtime", httpRes.Header.Get("X-OC-Mtime")) + w.Header().Set(HeaderOCMtime, httpRes.Header.Get(HeaderOCMtime)) } - w.Header().Set("Content-Type", info.MimeType) - w.Header().Set("OC-FileId", wrapResourceID(info.Id)) - w.Header().Set("OC-ETag", info.Etag) - w.Header().Set("ETag", info.Etag) + 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) t := utils.TSToTime(info.Mtime).UTC() lastModifiedString := t.Format(time.RFC1123Z) - w.Header().Set("Last-Modified", lastModifiedString) + w.Header().Set(HeaderLastModified, lastModifiedString) } } diff --git a/internal/http/services/owncloud/ocdav/versions.go b/internal/http/services/owncloud/ocdav/versions.go index af3644cbdf..a1e7e19162 100644 --- a/internal/http/services/owncloud/ocdav/versions.go +++ b/internal/http/services/owncloud/ocdav/versions.go @@ -62,11 +62,11 @@ func (h *VersionsHandler) Handler(s *svc, rid *provider.ResourceId) http.Handler s.handleOptions(w, r, "versions") return } - if key == "" && r.Method == "PROPFIND" { + if key == "" && r.Method == MethodPropfind { h.doListVersions(w, r, s, rid) return } - if key != "" && r.Method == "COPY" { + if key != "" && r.Method == MethodCopy { // TODO(jfd) it seems we cannot directly GET version content with cs3 ... // TODO(jfd) cs3api has no delete file version call // TODO(jfd) restore version to given Destination, but cs3api has no destination @@ -161,8 +161,8 @@ func (h *VersionsHandler) doListVersions(w http.ResponseWriter, r *http.Request, w.WriteHeader(http.StatusInternalServerError) return } - w.Header().Set("DAV", "1, 3, extended-mkcol") - w.Header().Set("Content-Type", "application/xml; charset=utf-8") + w.Header().Set(HeaderDav, "1, 3, extended-mkcol") + w.Header().Set(HeaderContentType, "application/xml; charset=utf-8") w.WriteHeader(http.StatusMultiStatus) _, err = w.Write([]byte(propRes)) if err != nil { diff --git a/internal/http/services/owncloud/ocdav/webdav.go b/internal/http/services/owncloud/ocdav/webdav.go index b8693ef97f..97b61510d7 100644 --- a/internal/http/services/owncloud/ocdav/webdav.go +++ b/internal/http/services/owncloud/ocdav/webdav.go @@ -23,6 +23,56 @@ import ( "path" ) +// Common Webdav methods. +// +// Unless otherwise noted, these are defined in RFC 4918 section 9. +const ( + MethodPropfind = "PROPFIND" + MethodLock = "LOCK" + MethodUnlock = "UNLOCK" + MethodProppatch = "PROPPATCH" + MethodMkcol = "MKCOL" + MethodMove = "MOVE" + MethodCopy = "COPY" + MethodReport = "REPORT" +) + +// Common HTTP headers. +const ( + HeaderAcceptRanges = "Accept-Ranges" + HeaderAccessControlAllowHeaders = "Access-Control-Allow-Headers" + HeaderAccessControlExposeHeaders = "Access-Control-Expose-Headers" + HeaderContentDisposistion = "Content-Disposition" + HeaderContentLength = "Content-Length" + HeaderContentRange = "Content-Range" + HeaderContentType = "Content-Type" + HeaderETag = "ETag" + HeaderLastModified = "Last-Modified" + HeaderLocation = "Location" + HeaderRange = "Range" + HeaderIfMatch = "If-Match" +) + +// Non standard HTTP headers. +const ( + HeaderOCFileID = "OC-FileId" + HeaderOCETag = "OC-ETag" + HeaderOCChecksum = "OC-Checksum" + HeaderDepth = "Depth" + HeaderDav = "DAV" + HeaderTusResumable = "Tus-Resumable" + HeaderTusVersion = "Tus-Version" + HeaderTusExtension = "Tus-Extension" + HeaderDestination = "Destination" + HeaderOverwrite = "Overwrite" + HeaderUploadChecksum = "Upload-Checksum" + HeaderUploadLength = "Upload-Length" + HeaderUploadMetadata = "Upload-Metadata" + HeaderUploadOffset = "Upload-Offset" + HeaderOCMtime = "X-OC-Mtime" + HeaderExpectedEntityLength = "X-Expected-Entity-Length" +) + // WebDavHandler implements a dav endpoint type WebDavHandler struct { namespace string @@ -40,34 +90,34 @@ func (h *WebDavHandler) Handler(s *svc) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ns := applyLayout(r.Context(), h.namespace, h.useLoggedInUserNS, r.URL.Path) switch r.Method { - case "PROPFIND": - s.handlePropfind(w, r, ns) - case "LOCK": + case MethodPropfind: + s.handlePathPropfind(w, r, ns) + case MethodLock: s.handleLock(w, r, ns) - case "UNLOCK": + case MethodUnlock: s.handleUnlock(w, r, ns) - case "PROPPATCH": - s.handleProppatch(w, r, ns) - case "MKCOL": - s.handleMkcol(w, r, ns) - case "MOVE": - s.handleMove(w, r, ns) - case "COPY": - s.handleCopy(w, r, ns) - case "REPORT": + case MethodProppatch: + s.handlePathProppatch(w, r, ns) + case MethodMkcol: + s.handlePathMkcol(w, r, ns) + case MethodMove: + s.handlePathMove(w, r, ns) + case MethodCopy: + s.handlePathCopy(w, r, ns) + case MethodReport: s.handleReport(w, r, ns) case http.MethodGet: - s.handleGet(w, r, ns) + s.handlePathGet(w, r, ns) case http.MethodPut: - s.handlePut(w, r, ns) + s.handlePathPut(w, r, ns) case http.MethodPost: - s.handleTusPost(w, r, ns) + s.handlePathTusPost(w, r, ns) case http.MethodOptions: s.handleOptions(w, r, ns) case http.MethodHead: - s.handleHead(w, r, ns) + s.handlePathHead(w, r, ns) case http.MethodDelete: - s.handleDelete(w, r, ns) + s.handlePathDelete(w, r, ns) default: w.WriteHeader(http.StatusNotFound) }