From 51a1d871625f32dc2931aff620fa9c40da2dab32 Mon Sep 17 00:00:00 2001 From: Vincent Petry Date: Wed, 24 Jun 2020 16:18:59 +0200 Subject: [PATCH] Fix public link file share MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Provide virtual container on Webdav layer for the public link share for a single file It is also possible to upload/overwrite the file. Co-authored-by: Jörn Friedrich Dreyer --- internal/http/services/owncloud/ocdav/dav.go | 66 +++++- .../http/services/owncloud/ocdav/options.go | 5 +- .../http/services/owncloud/ocdav/propfind.go | 12 +- .../services/owncloud/ocdav/publicfile.go | 216 ++++++++++++++++++ 4 files changed, 285 insertions(+), 14 deletions(-) create mode 100644 internal/http/services/owncloud/ocdav/publicfile.go diff --git a/internal/http/services/owncloud/ocdav/dav.go b/internal/http/services/owncloud/ocdav/dav.go index 9bd2501231..0d5d1d5df8 100644 --- a/internal/http/services/owncloud/ocdav/dav.go +++ b/internal/http/services/owncloud/ocdav/dav.go @@ -20,11 +20,15 @@ package ocdav import ( "context" + "fmt" "net/http" "path" gatewayv1beta1 "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" + rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" rpcv1beta1 "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/cs3org/reva/pkg/rgrpc/todo/pool" "github.com/cs3org/reva/pkg/rhttp/router" tokenpkg "github.com/cs3org/reva/pkg/token" @@ -32,13 +36,16 @@ import ( "google.golang.org/grpc/metadata" ) +type tokenStatInfoKey struct{} + // DavHandler routes to the different sub handlers type DavHandler struct { - AvatarsHandler *AvatarsHandler - FilesHandler *WebDavHandler - MetaHandler *MetaHandler - TrashbinHandler *TrashbinHandler - PublicFilesHandler *WebDavHandler + AvatarsHandler *AvatarsHandler + FilesHandler *WebDavHandler + MetaHandler *MetaHandler + TrashbinHandler *TrashbinHandler + PublicFolderHandler *WebDavHandler + PublicFileHandler *PublicFileHandler } func (h *DavHandler) init(c *Config) error { @@ -56,8 +63,13 @@ func (h *DavHandler) init(c *Config) error { } h.TrashbinHandler = new(TrashbinHandler) - h.PublicFilesHandler = new(WebDavHandler) - if err := h.PublicFilesHandler.init("public"); err != nil { // jail public file r equests to /public/ prefix + h.PublicFolderHandler = new(WebDavHandler) + if err := h.PublicFolderHandler.init("public"); err != nil { // jail public file requests to /public/ prefix + return err + } + + h.PublicFileHandler = new(PublicFileHandler) + if err := h.PublicFileHandler.init("public"); err != nil { // jail public file requests to /public/ prefix return err } @@ -68,6 +80,7 @@ func (h *DavHandler) init(c *Config) error { func (h *DavHandler) Handler(s *svc) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() + log := appctx.GetLogger(ctx) var head string head, r.URL.Path = router.ShiftPath(r.URL.Path) @@ -122,10 +135,47 @@ func (h *DavHandler) Handler(s *svc) http.Handler { ctx = metadata.AppendToOutgoingContext(ctx, tokenpkg.TokenHeader, res.Token) r = r.WithContext(ctx) - h.PublicFilesHandler.Handler(s).ServeHTTP(w, r) + + statInfo, err := getTokenStatInfo(ctx, c, token) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + log.Debug().Interface("statInfo", statInfo).Msg("Stat info from public link token path") + if statInfo.Type != provider.ResourceType_RESOURCE_TYPE_CONTAINER { + ctx := context.WithValue(ctx, tokenStatInfoKey{}, statInfo) + r = r.WithContext(ctx) + h.PublicFileHandler.Handler(s).ServeHTTP(w, r) + } else { + h.PublicFolderHandler.Handler(s).ServeHTTP(w, r) + } default: w.WriteHeader(http.StatusNotFound) } }) } + +func getTokenStatInfo(ctx context.Context, client gatewayv1beta1.GatewayAPIClient, token string) (*provider.ResourceInfo, error) { + ns := "/public" + + fn := path.Join(ns, token) + ref := &provider.Reference{ + Spec: &provider.Reference_Path{Path: fn}, + } + req := &provider.StatRequest{Ref: ref} + res, err := client.Stat(ctx, req) + if err != nil { + return nil, err + } + + if res.Status.Code != rpc.Code_CODE_OK { + return nil, fmt.Errorf("Failed to stat, status code %d: %s", res.Status.Code, res.Status.Message) + } + + if res.Info == nil { + return nil, fmt.Errorf("Failed to stat, info is nil") + } + + return res.Info, nil +} diff --git a/internal/http/services/owncloud/ocdav/options.go b/internal/http/services/owncloud/ocdav/options.go index 89d2ea9fb3..b3eb4e929a 100644 --- a/internal/http/services/owncloud/ocdav/options.go +++ b/internal/http/services/owncloud/ocdav/options.go @@ -20,6 +20,7 @@ package ocdav import ( "net/http" + "strings" ) func (s *svc) handleOptions(w http.ResponseWriter, r *http.Request, ns string) { @@ -27,11 +28,13 @@ func (s *svc) handleOptions(w http.ResponseWriter, r *http.Request, ns string) { allow += " MOVE, UNLOCK, PROPFIND, MKCOL, REPORT, SEARCH," allow += " PUT" // TODO(jfd): only for files ... but we cannot create the full path without a user ... which we only have when credentials are sent + isPublic := strings.Contains(r.Context().Value(ctxKeyBaseURI).(string), "public-files") + w.Header().Set("Content-Type", "application/xml") w.Header().Set("Allow", allow) w.Header().Set("DAV", "1, 2") w.Header().Set("MS-Author-Via", "DAV") - if !s.c.DisableTus { + if !s.c.DisableTus && !isPublic { w.Header().Add("Access-Control-Allow-Headers", "Tus-Resumable") w.Header().Add("Access-Control-Expose-Headers", "Tus-Resumable, Tus-Version, Tus-Extension") w.Header().Set("Tus-Resumable", "1.0.0") // TODO(jfd): only for dirs? diff --git a/internal/http/services/owncloud/ocdav/propfind.go b/internal/http/services/owncloud/ocdav/propfind.go index 315f1bc0eb..b02313ffd7 100644 --- a/internal/http/services/owncloud/ocdav/propfind.go +++ b/internal/http/services/owncloud/ocdav/propfind.go @@ -283,11 +283,13 @@ func (s *svc) mdToPropResponse(ctx context.Context, pf *propfindXML, md *provide Prop: []*propertyXML{}, }) - id := wrapResourceID(md.Id) - response.Propstat[0].Prop = append(response.Propstat[0].Prop, - s.newProp("oc:id", id), - s.newProp("oc:fileid", id), - ) + if md.Id != nil { + id := wrapResourceID(md.Id) + response.Propstat[0].Prop = append(response.Propstat[0].Prop, + s.newProp("oc:id", id), + s.newProp("oc:fileid", id), + ) + } if md.Etag != "" { // etags must be enclosed in double quotes and cannot contain them. diff --git a/internal/http/services/owncloud/ocdav/publicfile.go b/internal/http/services/owncloud/ocdav/publicfile.go new file mode 100644 index 0000000000..5de160381e --- /dev/null +++ b/internal/http/services/owncloud/ocdav/publicfile.go @@ -0,0 +1,216 @@ +// Copyright 2018-2020 CERN +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +package ocdav + +import ( + "net/http" + "path" + + provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" + "github.com/cs3org/reva/pkg/appctx" + "github.com/cs3org/reva/pkg/rhttp/router" + "go.opencensus.io/trace" +) + +// PublicFileHandler handles trashbin requests +type PublicFileHandler struct { + namespace string +} + +func (h *PublicFileHandler) init(ns string) error { + h.namespace = path.Join("/", ns) + return nil +} + +// Handler handles requests +func (h *PublicFileHandler) Handler(s *svc) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + log := appctx.GetLogger(r.Context()) + _, relativePath := router.ShiftPath(r.URL.Path) + + log.Debug().Str("relativePath", relativePath).Msg("PublicFileHandler func") + + if relativePath != "" && relativePath != "/" { + // accessing the file + // PROPFIND has an implicit call + if r.Method != "PROPFIND" && !s.adjustResourcePathInURL(w, r) { + return + } + + r.URL.Path = path.Base(r.URL.Path) + switch r.Method { + case "PROPFIND": + s.handlePropfindOnToken(w, r, h.namespace, false) + case http.MethodGet: + s.handleGet(w, r, h.namespace) + case http.MethodOptions: + s.handleOptions(w, r, h.namespace) + case http.MethodHead: + s.handleHead(w, r, h.namespace) + case http.MethodPut: + s.handlePut(w, r, h.namespace) + default: + w.WriteHeader(http.StatusMethodNotAllowed) + } + } else { + // accessing the virtual parent folder + switch r.Method { + case "PROPFIND": + 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) + default: + w.WriteHeader(http.StatusMethodNotAllowed) + } + } + }) +} + +func (s *svc) adjustResourcePathInURL(w http.ResponseWriter, r *http.Request) bool { + ctx := r.Context() + // find actual file name + log := appctx.GetLogger(ctx) + tokenStatInfo := ctx.Value(tokenStatInfoKey{}).(*provider.ResourceInfo) + client, err := s.getClient() + if err != nil { + log.Error().Err(err).Msg("error getting grpc client") + w.WriteHeader(http.StatusInternalServerError) + return false + } + pathRes, err := client.GetPath(ctx, &provider.GetPathRequest{ + ResourceId: tokenStatInfo.GetId(), + }) + if err != nil { + log.Warn(). + Str("tokenStatInfo.Id", tokenStatInfo.GetId().String()). + Str("tokenStatInfo.Path", tokenStatInfo.Path). + Msg("Could not get path of resource") + w.WriteHeader(http.StatusNotFound) + return false + } + if path.Base(r.URL.Path) != path.Base(pathRes.Path) { + w.WriteHeader(http.StatusNotFound) + return false + } + + // adjust path in request URL to point at the parent + r.URL.Path = path.Dir(r.URL.Path) + + return true +} + +// ns is the namespace that is prefixed to the path in the cs3 namespace +func (s *svc) handlePropfindOnToken(w http.ResponseWriter, r *http.Request, ns string, onContainer bool) { + ctx := r.Context() + ctx, span := trace.StartSpan(ctx, "propfind") + defer span.End() + log := appctx.GetLogger(ctx) + + tokenStatInfo := ctx.Value(tokenStatInfoKey{}).(*provider.ResourceInfo) + log.Debug().Interface("tokenStatInfo", tokenStatInfo).Msg("handlePropfindOnToken") + + depth := r.Header.Get("Depth") + if depth == "" { + depth = "1" + } + + // see https://tools.ietf.org/html/rfc4918#section-10.2 + if depth != "0" && depth != "1" && depth != "infinity" { + log.Error().Msgf("invalid Depth header value %s", depth) + w.WriteHeader(http.StatusBadRequest) + return + } + + pf, status, err := readPropfind(r.Body) + if err != nil { + log.Error().Err(err).Msg("error reading propfind request") + w.WriteHeader(status) + return + } + + client, err := s.getClient() + if err != nil { + log.Error().Err(err).Msg("error getting grpc client") + w.WriteHeader(http.StatusInternalServerError) + return + } + + // find actual file name + pathRes, err := client.GetPath(ctx, &provider.GetPathRequest{ + ResourceId: tokenStatInfo.GetId(), + }) + if err != nil { + log.Warn(). + Str("tokenStatInfo.Id", tokenStatInfo.GetId().String()). + Str("tokenStatInfo.Path", tokenStatInfo.Path). + Msg("Could not get path of resource") + w.WriteHeader(http.StatusNotFound) + return + } + + infos := []*provider.ResourceInfo{} + + if onContainer { + // TODO: filter out metadata like favorite and arbitrary metadata + if depth != "0" { + // if the request is to a public link, we need to add yet another value for the file entry. + infos = append(infos, &provider.ResourceInfo{ + // append the shared as a container. Annex to OC10 standards. + Id: tokenStatInfo.Id, + Path: tokenStatInfo.Path, + Type: provider.ResourceType_RESOURCE_TYPE_CONTAINER, + Mtime: tokenStatInfo.Mtime, + Size: tokenStatInfo.Size, + Etag: tokenStatInfo.Etag, + PermissionSet: tokenStatInfo.PermissionSet, + }) + } + } else if path.Base(r.URL.Path) != path.Base(pathRes.Path) { + // if queried on the wrong path, return not found + w.WriteHeader(http.StatusNotFound) + return + } + + infos = append(infos, &provider.ResourceInfo{ + Id: tokenStatInfo.Id, + Path: path.Join("/", tokenStatInfo.Path, path.Base(pathRes.Path)), + Type: tokenStatInfo.Type, + Size: tokenStatInfo.Size, + MimeType: tokenStatInfo.MimeType, + Mtime: tokenStatInfo.Mtime, + Etag: tokenStatInfo.Etag, + PermissionSet: tokenStatInfo.PermissionSet, + }) + + propRes, err := s.formatPropfind(ctx, &pf, infos, ns) + if err != nil { + log.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") + w.WriteHeader(http.StatusMultiStatus) + if _, err := w.Write([]byte(propRes)); err != nil { + log.Err(err).Msg("error writing response") + } +}