Skip to content

Commit

Permalink
Fix public link file share (#895)
Browse files Browse the repository at this point in the history
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 <jfd@butonic.de>

Co-authored-by: Jörn Friedrich Dreyer <jfd@butonic.de>
  • Loading branch information
Vincent Petry and butonic authored Jun 29, 2020
1 parent 6f16982 commit e33d652
Show file tree
Hide file tree
Showing 4 changed files with 285 additions and 14 deletions.
66 changes: 58 additions & 8 deletions internal/http/services/owncloud/ocdav/dav.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,25 +20,32 @@ 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"
"github.com/cs3org/reva/pkg/user"
"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 {
Expand All @@ -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
}

Expand All @@ -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)
Expand Down Expand Up @@ -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
}
5 changes: 4 additions & 1 deletion internal/http/services/owncloud/ocdav/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,21 @@ package ocdav

import (
"net/http"
"strings"
)

func (s *svc) handleOptions(w http.ResponseWriter, r *http.Request, ns string) {
allow := "OPTIONS, LOCK, GET, HEAD, POST, DELETE, PROPPATCH, COPY,"
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?
Expand Down
12 changes: 7 additions & 5 deletions internal/http/services/owncloud/ocdav/propfind.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
216 changes: 216 additions & 0 deletions internal/http/services/owncloud/ocdav/publicfile.go
Original file line number Diff line number Diff line change
@@ -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")
}
}

0 comments on commit e33d652

Please sign in to comment.