Use the opencloud reva from now on

This commit is contained in:
André Duffeck
2025-01-21 11:04:04 +01:00
parent 2c1afafb35
commit e8d35e1280
1007 changed files with 2988 additions and 27822 deletions
@@ -0,0 +1,60 @@
// Copyright 2018-2021 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 appctx creates a context with useful
// components attached to the context like loggers and
// token managers.
package appctx
import (
"net/http"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
"github.com/rs/zerolog"
"go.opentelemetry.io/otel/trace"
)
// name is the Tracer name used to identify this instrumentation library.
const tracerName = "appctx"
// New returns a new HTTP middleware that stores the log
// in the context with request ID information.
func New(log zerolog.Logger, tp trace.TracerProvider) func(http.Handler) http.Handler {
chain := func(h http.Handler) http.Handler {
return handler(log, tp, h)
}
return chain
}
func handler(log zerolog.Logger, tp trace.TracerProvider, h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
span := trace.SpanFromContext(ctx)
defer span.End()
if !span.SpanContext().HasTraceID() {
ctx, span = tp.Tracer(tracerName).Start(ctx, "http interceptor")
}
sub := log.With().Str("traceid", span.SpanContext().TraceID().String()).Logger()
ctx = appctx.WithLogger(ctx, &sub)
ctx = appctx.WithTracerProvider(ctx, tp)
r = r.WithContext(ctx)
h.ServeHTTP(w, r)
})
}
@@ -0,0 +1,420 @@
// Copyright 2018-2021 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 auth
import (
"context"
"fmt"
"net/http"
"strings"
"sync"
"time"
"github.com/bluele/gcache"
authpb "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
"github.com/mitchellh/mapstructure"
"github.com/opencloud-eu/reva/v2/internal/http/interceptors/auth/credential/registry"
tokenregistry "github.com/opencloud-eu/reva/v2/internal/http/interceptors/auth/token/registry"
tokenwriterregistry "github.com/opencloud-eu/reva/v2/internal/http/interceptors/auth/tokenwriter/registry"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
"github.com/opencloud-eu/reva/v2/pkg/auth"
"github.com/opencloud-eu/reva/v2/pkg/auth/scope"
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/opencloud-eu/reva/v2/pkg/rhttp/global"
"github.com/opencloud-eu/reva/v2/pkg/sharedconf"
"github.com/opencloud-eu/reva/v2/pkg/token"
tokenmgr "github.com/opencloud-eu/reva/v2/pkg/token/manager/registry"
"github.com/opencloud-eu/reva/v2/pkg/utils"
"github.com/pkg/errors"
"github.com/rs/zerolog"
semconv "go.opentelemetry.io/otel/semconv/v1.20.0"
"go.opentelemetry.io/otel/trace"
"google.golang.org/grpc/metadata"
)
// name is the Tracer name used to identify this instrumentation library.
const tracerName = "auth"
var (
cacheOnce sync.Once
userGroupsCache gcache.Cache
)
type config struct {
Priority int `mapstructure:"priority"`
GatewaySvc string `mapstructure:"gatewaysvc"`
// TODO(jdf): Realm is optional, will be filled with request host if not given?
Realm string `mapstructure:"realm"`
CredentialsByUserAgent map[string]string `mapstructure:"credentials_by_user_agent"`
CredentialChain []string `mapstructure:"credential_chain"`
CredentialStrategies map[string]map[string]interface{} `mapstructure:"credential_strategies"`
TokenStrategyChain []string `mapstructure:"token_strategy_chain"`
TokenStrategies map[string]map[string]interface{} `mapstructure:"token_strategies"`
TokenManager string `mapstructure:"token_manager"`
TokenManagers map[string]map[string]interface{} `mapstructure:"token_managers"`
TokenWriter string `mapstructure:"token_writer"`
TokenWriters map[string]map[string]interface{} `mapstructure:"token_writers"`
UserGroupsCacheSize int `mapstructure:"usergroups_cache_size"`
}
func parseConfig(m map[string]interface{}) (*config, error) {
c := &config{}
if err := mapstructure.Decode(m, c); err != nil {
err = errors.Wrap(err, "error decoding conf")
return nil, err
}
return c, nil
}
// New returns a new middleware with defined priority.
func New(m map[string]interface{}, unprotected []string, tp trace.TracerProvider) (global.Middleware, error) {
conf, err := parseConfig(m)
if err != nil {
return nil, err
}
conf.GatewaySvc = sharedconf.GetGatewaySVC(conf.GatewaySvc)
// set defaults
if len(conf.TokenStrategyChain) == 0 {
conf.TokenStrategyChain = []string{"header"}
}
if conf.TokenWriter == "" {
conf.TokenWriter = "header"
}
if conf.TokenManager == "" {
conf.TokenManager = "jwt"
}
if conf.CredentialsByUserAgent == nil {
conf.CredentialsByUserAgent = map[string]string{}
}
if conf.UserGroupsCacheSize == 0 {
conf.UserGroupsCacheSize = 5000
}
cacheOnce.Do(func() {
userGroupsCache = gcache.New(conf.UserGroupsCacheSize).LFU().Build()
})
credChain := map[string]auth.CredentialStrategy{}
for i, key := range conf.CredentialChain {
f, ok := registry.NewCredentialFuncs[conf.CredentialChain[i]]
if !ok {
return nil, fmt.Errorf("credential strategy not found: %s", conf.CredentialChain[i])
}
credStrategy, err := f(conf.CredentialStrategies[conf.CredentialChain[i]])
if err != nil {
return nil, err
}
credChain[key] = credStrategy
}
tokenStrategyChain := make([]auth.TokenStrategy, 0, len(conf.TokenStrategyChain))
for _, strategy := range conf.TokenStrategyChain {
g, ok := tokenregistry.NewTokenFuncs[strategy]
if !ok {
return nil, fmt.Errorf("token strategy not found: %s", strategy)
}
tokenStrategy, err := g(conf.TokenStrategies[strategy])
if err != nil {
return nil, err
}
tokenStrategyChain = append(tokenStrategyChain, tokenStrategy)
}
h, ok := tokenmgr.NewFuncs[conf.TokenManager]
if !ok {
return nil, fmt.Errorf("token manager not found: %s", conf.TokenManager)
}
tokenManager, err := h(conf.TokenManagers[conf.TokenManager])
if err != nil {
return nil, err
}
i, ok := tokenwriterregistry.NewTokenFuncs[conf.TokenWriter]
if !ok {
return nil, fmt.Errorf("token writer not found: %s", conf.TokenWriter)
}
tokenWriter, err := i(conf.TokenWriters[conf.TokenWriter])
if err != nil {
return nil, err
}
chain := func(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// OPTION requests need to pass for preflight requests
// TODO(labkode): this will break options for auth protected routes.
// Maybe running the CORS middleware before auth kicks in is enough.
ctx := r.Context()
span := trace.SpanFromContext(ctx)
defer span.End()
if !span.SpanContext().HasTraceID() {
_, span = tp.Tracer(tracerName).Start(ctx, "http auth interceptor")
}
if r.Method == "OPTIONS" {
h.ServeHTTP(w, r)
return
}
log := appctx.GetLogger(r.Context())
isUnprotectedEndpoint := false
// For unprotected URLs, we try to authenticate the request in case some service needs it,
// but don't return any errors if it fails.
if utils.Skip(r.URL.Path, unprotected) {
log.Info().Msg("skipping auth check for: " + r.URL.Path)
isUnprotectedEndpoint = true
}
ctx, err := authenticateUser(w, r, conf, tokenStrategyChain, tokenManager, tokenWriter, credChain, isUnprotectedEndpoint)
if err != nil {
if !isUnprotectedEndpoint {
return
}
} else {
u, ok := ctxpkg.ContextGetUser(ctx)
if ok {
span.SetAttributes(semconv.EnduserIDKey.String(u.Id.OpaqueId))
}
r = r.WithContext(ctx)
}
h.ServeHTTP(w, r)
})
}
return chain, nil
}
func authenticateUser(w http.ResponseWriter, r *http.Request, conf *config, tokenStrategies []auth.TokenStrategy, tokenManager token.Manager, tokenWriter auth.TokenWriter, credChain map[string]auth.CredentialStrategy, isUnprotectedEndpoint bool) (context.Context, error) {
ctx := r.Context()
log := appctx.GetLogger(ctx)
// Add the request user-agent to the ctx
ctx = metadata.NewIncomingContext(ctx, metadata.New(map[string]string{ctxpkg.UserAgentHeader: r.UserAgent()}))
client, err := pool.GetGatewayServiceClient(conf.GatewaySvc)
if err != nil {
logError(isUnprotectedEndpoint, log, err, "error getting the authsvc client", http.StatusUnauthorized, w)
return nil, err
}
// reva token or auth token can be passed using the same technique (for example bearer)
// before validating it against an auth provider, we can check directly if it's a reva
// token and if not try to use it for authenticating the user.
for _, tokenStrategy := range tokenStrategies {
token := tokenStrategy.GetToken(r)
if token != "" {
if user, tokenScope, ok := isTokenValid(r, tokenManager, token); ok {
if err := insertGroupsInUser(ctx, userGroupsCache, client, user); err != nil {
logError(isUnprotectedEndpoint, log, err, "got an error retrieving groups for user "+user.Username, http.StatusInternalServerError, w)
return nil, err
}
return ctxWithUserInfo(ctx, r, user, token, tokenScope, r.Header.Get(ctxpkg.InitiatorHeader)), nil
}
}
}
log.Warn().Msg("core access token not set")
userAgentCredKeys := getCredsForUserAgent(r.UserAgent(), conf.CredentialsByUserAgent, conf.CredentialChain)
// obtain credentials (basic auth, bearer token, ...) based on user agent
var creds *auth.Credentials
for _, k := range userAgentCredKeys {
creds, err = credChain[k].GetCredentials(w, r)
if err != nil {
log.Debug().Err(err).Msg("error retrieving credentials")
}
if creds != nil {
log.Debug().Msgf("credentials obtained from credential strategy: type: %s, client_id: %s", creds.Type, creds.ClientID)
break
}
}
// if no credentials are found, reply with authentication challenge depending on user agent
if creds == nil {
if !isUnprotectedEndpoint {
for _, key := range userAgentCredKeys {
if cred, ok := credChain[key]; ok {
cred.AddWWWAuthenticate(w, r, conf.Realm)
} else {
log.Error().Msg("auth credential strategy: " + key + "must have been loaded in init method")
w.WriteHeader(http.StatusInternalServerError)
return nil, errtypes.InternalError("no credentials found")
}
}
w.WriteHeader(http.StatusUnauthorized)
}
return nil, errtypes.PermissionDenied("no credentials found")
}
req := &gateway.AuthenticateRequest{
Type: creds.Type,
ClientId: creds.ClientID,
ClientSecret: creds.ClientSecret,
}
log.Debug().Msgf("AuthenticateRequest: type: %s, client_id: %s against %s", req.Type, req.ClientId, conf.GatewaySvc)
res, err := client.Authenticate(ctx, req)
if err != nil {
logError(isUnprotectedEndpoint, log, err, "error calling Authenticate", http.StatusUnauthorized, w)
return nil, err
}
if res.Status.Code != rpc.Code_CODE_OK {
err := status.NewErrorFromCode(res.Status.Code, "auth")
logError(isUnprotectedEndpoint, log, err, "error generating access token from credentials", http.StatusUnauthorized, w)
return nil, err
}
log.Info().Msg("core access token generated") // write token to response
// write token to response
token := res.Token
tokenWriter.WriteToken(token, w)
// validate token
u, tokenScope, err := tokenManager.DismantleToken(r.Context(), token)
if err != nil {
logError(isUnprotectedEndpoint, log, err, "error dismantling token", http.StatusUnauthorized, w)
return nil, err
}
if sharedconf.SkipUserGroupsInToken() {
var groups []string
if groupsIf, err := userGroupsCache.Get(u.Id.OpaqueId); err == nil {
groups = groupsIf.([]string)
} else {
groupsRes, err := client.GetUserGroups(ctx, &userpb.GetUserGroupsRequest{UserId: u.Id})
if err != nil {
logError(isUnprotectedEndpoint, log, err, "error retrieving user groups", http.StatusInternalServerError, w)
return nil, err
}
groups = groupsRes.Groups
_ = userGroupsCache.SetWithExpire(u.Id.OpaqueId, groupsRes.Groups, 3600*time.Second)
}
u.Groups = groups
}
// ensure access to the resource is allowed
ok, err := scope.VerifyScope(ctx, tokenScope, r.URL.Path)
if err != nil {
logError(isUnprotectedEndpoint, log, err, "error verifying scope of access token", http.StatusInternalServerError, w)
return nil, err
}
if !ok {
err := errtypes.PermissionDenied("access to resource not allowed")
logError(isUnprotectedEndpoint, log, err, "access to resource not allowed", http.StatusUnauthorized, w)
return nil, err
}
return ctxWithUserInfo(ctx, r, u, token, tokenScope, r.Header.Get(ctxpkg.InitiatorHeader)), nil
}
func ctxWithUserInfo(ctx context.Context, r *http.Request, user *userpb.User, token string, tokenScope map[string]*authpb.Scope, initiatorid string) context.Context {
ctx = ctxpkg.ContextSetUser(ctx, user)
ctx = ctxpkg.ContextSetToken(ctx, token)
ctx = ctxpkg.ContextSetInitiator(ctx, initiatorid)
ctx = metadata.AppendToOutgoingContext(ctx, ctxpkg.TokenHeader, token)
ctx = metadata.AppendToOutgoingContext(ctx, ctxpkg.UserAgentHeader, r.UserAgent())
ctx = metadata.AppendToOutgoingContext(ctx, ctxpkg.InitiatorHeader, initiatorid)
ctx = ctxpkg.ContextSetScopes(ctx, tokenScope)
return ctx
}
func insertGroupsInUser(ctx context.Context, userGroupsCache gcache.Cache, client gateway.GatewayAPIClient, user *userpb.User) error {
if sharedconf.SkipUserGroupsInToken() {
var groups []string
if groupsIf, err := userGroupsCache.Get(user.Id.OpaqueId); err == nil {
groups = groupsIf.([]string)
} else {
groupsRes, err := client.GetUserGroups(ctx, &userpb.GetUserGroupsRequest{UserId: user.Id})
if err != nil {
return err
}
groups = groupsRes.Groups
_ = userGroupsCache.SetWithExpire(user.Id.OpaqueId, groupsRes.Groups, 3600*time.Second)
}
user.Groups = groups
}
return nil
}
func isTokenValid(r *http.Request, tokenManager token.Manager, token string) (*userpb.User, map[string]*authpb.Scope, bool) {
ctx := r.Context()
u, tokenScope, err := tokenManager.DismantleToken(ctx, token)
if err != nil {
return nil, nil, false
}
// ensure access to the resource is allowed
ok, err := scope.VerifyScope(ctx, tokenScope, r.URL.Path)
if err != nil {
return nil, nil, false
}
return u, tokenScope, ok
}
func logError(isUnprotectedEndpoint bool, log *zerolog.Logger, err error, msg string, status int, w http.ResponseWriter) {
if !isUnprotectedEndpoint {
log.Error().Err(err).Msg(msg)
w.WriteHeader(status)
}
}
// getCredsForUserAgent returns the WWW Authenticate challenges keys to use given an http request
// and available credentials.
func getCredsForUserAgent(ua string, uam map[string]string, creds []string) []string {
if ua == "" || len(uam) == 0 {
return creds
}
for u, cred := range uam {
if strings.Contains(ua, u) {
for _, v := range creds {
if v == cred {
return []string{cred}
}
}
return creds
}
}
return creds
}
@@ -0,0 +1,27 @@
// Copyright 2018-2021 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 loader
import (
// Load core authentication strategies.
_ "github.com/opencloud-eu/reva/v2/internal/http/interceptors/auth/credential/strategy/basic"
_ "github.com/opencloud-eu/reva/v2/internal/http/interceptors/auth/credential/strategy/bearer"
_ "github.com/opencloud-eu/reva/v2/internal/http/interceptors/auth/credential/strategy/ocmshares"
// Add your own here.
)
@@ -0,0 +1,36 @@
// Copyright 2018-2021 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 registry
import (
"github.com/opencloud-eu/reva/v2/pkg/auth"
)
// NewCredentialFunc is the function that credential strategies
// should register at init time.
type NewCredentialFunc func(map[string]interface{}) (auth.CredentialStrategy, error)
// NewCredentialFuncs is a map containing all the registered auth strategies.
var NewCredentialFuncs = map[string]NewCredentialFunc{}
// Register registers a new auth strategy new function.
// Not safe for concurrent use. Safe for use from package init.
func Register(name string, f NewCredentialFunc) {
NewCredentialFuncs[name] = f
}
@@ -0,0 +1,56 @@
// Copyright 2018-2021 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 basic
import (
"fmt"
"net/http"
"github.com/opencloud-eu/reva/v2/internal/http/interceptors/auth/credential/registry"
"github.com/opencloud-eu/reva/v2/pkg/auth"
)
func init() {
registry.Register("basic", New)
}
type strategy struct{}
// New returns a new auth strategy that checks for basic auth.
// See https://tools.ietf.org/html/rfc7617
func New(m map[string]interface{}) (auth.CredentialStrategy, error) {
return &strategy{}, nil
}
func (s *strategy) GetCredentials(w http.ResponseWriter, r *http.Request) (*auth.Credentials, error) {
id, secret, ok := r.BasicAuth()
if !ok {
return nil, fmt.Errorf("no basic auth provided")
}
return &auth.Credentials{Type: "basic", ClientID: id, ClientSecret: secret}, nil
}
func (s *strategy) AddWWWAuthenticate(w http.ResponseWriter, r *http.Request, realm string) {
// TODO read realm from forwarded header?
if realm == "" {
// fall back to hostname if not configured
realm = r.Host
}
w.Header().Add("WWW-Authenticate", fmt.Sprintf(`Basic realm="%s"`, realm))
}
@@ -0,0 +1,67 @@
// Copyright 2018-2021 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 bearer
import (
"fmt"
"net/http"
"strings"
"github.com/opencloud-eu/reva/v2/internal/http/interceptors/auth/credential/registry"
"github.com/opencloud-eu/reva/v2/pkg/auth"
)
func init() {
registry.Register("bearer", New)
}
type strategy struct{}
// New returns a new auth strategy that checks "Bearer" OAuth Access Tokens
// See https://tools.ietf.org/html/rfc6750#section-6.1
func New(m map[string]interface{}) (auth.CredentialStrategy, error) {
return &strategy{}, nil
}
func (s *strategy) GetCredentials(w http.ResponseWriter, r *http.Request) (*auth.Credentials, error) {
// 1. check Authorization header
hdr := r.Header.Get("Authorization")
token := strings.TrimPrefix(hdr, "Bearer ")
if token != "" {
return &auth.Credentials{Type: "bearer", ClientSecret: token}, nil
}
// TODO 2. check form encoded body parameter for POST requests, see https://tools.ietf.org/html/rfc6750#section-2.2
// 3. check uri query parameter, see https://tools.ietf.org/html/rfc6750#section-2.3
tokens, ok := r.URL.Query()["access_token"]
if !ok || len(tokens[0]) < 1 {
return nil, fmt.Errorf("no bearer auth provided")
}
return &auth.Credentials{Type: "bearer", ClientSecret: tokens[0]}, nil
}
func (s *strategy) AddWWWAuthenticate(w http.ResponseWriter, r *http.Request, realm string) {
// TODO read realm from forwarded header?
if realm == "" {
// fall back to hostname if not configured
realm = r.Host
}
w.Header().Add("WWW-Authenticate", fmt.Sprintf(`Bearer realm="%s"`, realm))
}
@@ -0,0 +1,58 @@
// Copyright 2018-2023 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 ocmshares
import (
"fmt"
"net/http"
"github.com/opencloud-eu/reva/v2/internal/http/interceptors/auth/credential/registry"
"github.com/opencloud-eu/reva/v2/pkg/auth"
)
func init() {
registry.Register("ocmshares", New)
}
const (
headerShareToken = "ocm-token"
)
type strategy struct{}
// New returns a new auth strategy that handles public share verification.
func New(m map[string]interface{}) (auth.CredentialStrategy, error) {
return &strategy{}, nil
}
func (s *strategy) GetCredentials(w http.ResponseWriter, r *http.Request) (*auth.Credentials, error) {
token := r.Header.Get(headerShareToken)
if token == "" {
token = r.URL.Query().Get(headerShareToken)
}
if token == "" {
return nil, fmt.Errorf("no ocm token provided")
}
return &auth.Credentials{Type: "ocmshares", ClientID: token}, nil
}
func (s *strategy) AddWWWAuthenticate(w http.ResponseWriter, r *http.Request, realm string) {
// TODO read realm from forwarded header?
}
@@ -0,0 +1,26 @@
// Copyright 2018-2021 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 loader
import (
// Load core token strategies.
_ "github.com/opencloud-eu/reva/v2/internal/http/interceptors/auth/token/strategy/bearer"
_ "github.com/opencloud-eu/reva/v2/internal/http/interceptors/auth/token/strategy/header"
// Add your own here.
)
@@ -0,0 +1,34 @@
// Copyright 2018-2021 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 registry
import "github.com/opencloud-eu/reva/v2/pkg/auth"
// NewTokenFunc is the function that token strategies
// should register at init time.
type NewTokenFunc func(map[string]interface{}) (auth.TokenStrategy, error)
// NewTokenFuncs is a map containing all the registered auth strategies.
var NewTokenFuncs = map[string]NewTokenFunc{}
// Register registers a new auth strategy new function.
// Not safe for concurrent use. Safe for use from package init.
func Register(name string, f NewTokenFunc) {
NewTokenFuncs[name] = f
}
@@ -0,0 +1,84 @@
// Copyright 2018-2023 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 header
import (
"mime"
"net/http"
"strings"
"github.com/opencloud-eu/reva/v2/internal/http/interceptors/auth/token/registry"
"github.com/opencloud-eu/reva/v2/pkg/auth"
)
func init() {
registry.Register("bearer", New)
}
type b struct{}
// New returns a new auth strategy that checks for bearer auth.
func New(m map[string]interface{}) (auth.TokenStrategy, error) {
return b{}, nil
}
func (b) GetToken(r *http.Request) string {
// Authorization Request Header Field: https://www.rfc-editor.org/rfc/rfc6750#section-2.1
if tkn, ok := getFromAuthorizationHeader(r); ok {
return tkn
}
// Form-Encoded Body Parameter: https://www.rfc-editor.org/rfc/rfc6750#section-2.2
if tkn, ok := getFromBody(r); ok {
return tkn
}
// URI Query Parameter: https://www.rfc-editor.org/rfc/rfc6750#section-2.3
if tkn, ok := getFromQueryParam(r); ok {
return tkn
}
return ""
}
func getFromAuthorizationHeader(r *http.Request) (string, bool) {
auth := r.Header.Get("Authorization")
tkn := strings.TrimPrefix(auth, "Bearer ")
return tkn, tkn != ""
}
func getFromBody(r *http.Request) (string, bool) {
mediatype, _, err := mime.ParseMediaType(r.Header.Get("content-type"))
if err != nil {
return "", false
}
if mediatype != "application/x-www-form-urlencoded" {
return "", false
}
if err = r.ParseForm(); err != nil {
return "", false
}
tkn := r.Form.Get("access-token")
return tkn, tkn != ""
}
func getFromQueryParam(r *http.Request) (string, bool) {
tkn := r.URL.Query().Get("access_token")
return tkn, tkn != ""
}
@@ -0,0 +1,44 @@
// Copyright 2018-2021 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 header
import (
"net/http"
"github.com/opencloud-eu/reva/v2/internal/http/interceptors/auth/token/registry"
"github.com/opencloud-eu/reva/v2/pkg/auth"
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
)
func init() {
registry.Register("header", New)
}
type strategy struct {
header string
}
// New returns a new auth strategy that checks for basic auth.
func New(m map[string]interface{}) (auth.TokenStrategy, error) {
return &strategy{header: ctxpkg.TokenHeader}, nil
}
func (s *strategy) GetToken(r *http.Request) string {
return r.Header.Get(s.header)
}
@@ -0,0 +1,25 @@
// Copyright 2018-2021 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 loader
import (
// Load core token writer strategies.
_ "github.com/opencloud-eu/reva/v2/internal/http/interceptors/auth/tokenwriter/strategy/header"
// Add your own here.
)
@@ -0,0 +1,34 @@
// Copyright 2018-2021 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 registry
import "github.com/opencloud-eu/reva/v2/pkg/auth"
// NewTokenFunc is the function that token writers
// should register at init time.
type NewTokenFunc func(map[string]interface{}) (auth.TokenWriter, error)
// NewTokenFuncs is a map containing all the registered token writers.
var NewTokenFuncs = map[string]NewTokenFunc{}
// Register registers a new token writer strategy new function.
// Not safe for concurrent use. Safe for use from package init.
func Register(name string, f NewTokenFunc) {
NewTokenFuncs[name] = f
}
@@ -0,0 +1,44 @@
// Copyright 2018-2021 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 header
import (
"net/http"
"github.com/opencloud-eu/reva/v2/internal/http/interceptors/auth/tokenwriter/registry"
"github.com/opencloud-eu/reva/v2/pkg/auth"
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
)
func init() {
registry.Register("header", New)
}
type strategy struct {
header string
}
// New returns a new token writer strategy that stores token in a header.
func New(m map[string]interface{}) (auth.TokenWriter, error) {
return &strategy{header: ctxpkg.TokenHeader}, nil
}
func (s *strategy) WriteToken(token string, w http.ResponseWriter) {
w.Header().Set(s.header, token)
}
@@ -0,0 +1,128 @@
// Copyright 2018-2021 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 cors
import (
"github.com/mitchellh/mapstructure"
"github.com/opencloud-eu/reva/v2/pkg/rhttp/global"
"github.com/rs/cors"
)
const (
defaultPriority = 200
)
func init() {
global.RegisterMiddleware("cors", New)
}
type config struct {
AllowCredentials bool `mapstructure:"allow_credentials"`
OptionsPassthrough bool `mapstructure:"options_passthrough"`
Debug bool `mapstructure:"debug"`
MaxAge int `mapstructure:"max_age"`
Priority int `mapstructure:"priority"`
AllowedMethods []string `mapstructure:"allowed_methods"`
AllowedHeaders []string `mapstructure:"allowed_headers"`
ExposedHeaders []string `mapstructure:"exposed_headers"`
AllowedOrigins []string `mapstructure:"allowed_origins"`
}
// New creates a new CORS middleware.
func New(m map[string]interface{}) (global.Middleware, int, error) {
conf := &config{}
if err := mapstructure.Decode(m, conf); err != nil {
return nil, 0, err
}
if conf.Priority == 0 {
conf.Priority = defaultPriority
}
// apply some defaults to reduce configuration boilerplate
if len(conf.AllowedOrigins) == 0 {
conf.AllowedOrigins = []string{"*"}
}
if len(conf.AllowedMethods) == 0 {
conf.AllowedMethods = []string{
"OPTIONS",
"HEAD",
"GET",
"PUT",
"POST",
"DELETE",
"MKCOL",
"PROPFIND",
"PROPPATCH",
"MOVE",
"COPY",
"REPORT",
"SEARCH",
}
}
if len(conf.AllowedHeaders) == 0 {
conf.AllowedHeaders = []string{
"Origin",
"Accept",
"Content-Type",
"Depth",
"Authorization",
"Ocs-Apirequest",
"If-None-Match",
"If-Match",
"Destination",
"Overwrite",
"X-Request-Id",
"X-Requested-With",
"Tus-Resumable",
"Tus-Checksum-Algorithm",
"Upload-Concat",
"Upload-Length",
"Upload-Metadata",
"Upload-Defer-Length",
"Upload-Expires",
"Upload-Checksum",
"Upload-Offset",
"X-HTTP-Method-Override",
}
}
if len(conf.ExposedHeaders) == 0 {
conf.ExposedHeaders = []string{
"Location",
}
}
// TODO(jfd): use log from request context, otherwise fmt will be used to log,
// preventing us from piping the log to eg jq
c := cors.New(cors.Options{
AllowCredentials: conf.AllowCredentials,
AllowedHeaders: conf.AllowedHeaders,
AllowedMethods: conf.AllowedMethods,
AllowedOrigins: conf.AllowedOrigins,
ExposedHeaders: conf.ExposedHeaders,
MaxAge: conf.MaxAge,
OptionsPassthrough: conf.OptionsPassthrough,
Debug: conf.Debug,
})
return c.Handler, conf.Priority, nil
}
@@ -0,0 +1,28 @@
// Copyright 2018-2021 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 loader
import (
// Load core HTTP middlewares.
_ "github.com/opencloud-eu/reva/v2/internal/http/interceptors/cors"
_ "github.com/opencloud-eu/reva/v2/internal/http/interceptors/prometheus"
_ "github.com/opencloud-eu/reva/v2/internal/http/interceptors/providerauthorizer"
_ "github.com/opencloud-eu/reva/v2/internal/http/interceptors/requestid"
// Add your own middleware.
)
@@ -0,0 +1,195 @@
// Copyright 2018-2021 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 log
import (
"bufio"
"fmt"
"net"
"net/http"
"net/url"
"time"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
"github.com/rs/zerolog"
)
// New returns a new HTTP middleware that logs HTTP requests and responses.
// TODO(labkode): maybe log to another file?
func New() func(http.Handler) http.Handler {
return handler
}
// handler is a logging middleware
func handler(h http.Handler) http.Handler {
return newLoggingHandler(h)
}
func newLoggingHandler(h http.Handler) http.Handler {
return loggingHandler{handler: h}
}
type loggingHandler struct {
handler http.Handler
}
func (h loggingHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
log := appctx.GetLogger(req.Context())
t := time.Now()
logger := makeLogger(w)
url := *req.URL
h.handler.ServeHTTP(logger, req)
writeLog(log, req, url, t, logger.Status(), logger.Size())
}
func makeLogger(w http.ResponseWriter) loggingResponseWriter {
var logger loggingResponseWriter = &responseLogger{w: w, status: http.StatusOK}
if _, ok := w.(http.Hijacker); ok {
logger = &hijackLogger{responseLogger{w: w, status: http.StatusOK}}
}
h, ok1 := logger.(http.Hijacker)
c, ok2 := w.(http.CloseNotifier)
if ok1 && ok2 {
return hijackCloseNotifier{logger, h, c}
}
if ok2 {
return &closeNotifyWriter{logger, c}
}
return logger
}
func writeLog(log *zerolog.Logger, req *http.Request, url url.URL, ts time.Time, status, size int) {
end := time.Now()
host, _, err := net.SplitHostPort(req.RemoteAddr)
if err != nil {
host = req.RemoteAddr
}
uri := req.RequestURI
u := req.URL.String()
if req.ProtoMajor == 2 && req.Method == "CONNECT" {
uri = req.Host
}
if uri == "" {
uri = url.RequestURI()
}
diff := end.Sub(ts).Nanoseconds()
var event *zerolog.Event
switch {
case status < 400:
event = log.Debug()
case status < 500:
event = log.Warn()
default:
event = log.Error()
}
event.Str("host", host).Str("method", req.Method).
Str("uri", uri).Str("url", u).Str("proto", req.Proto).Int("status", status).
Int("size", size).
Str("start", ts.Format("02/Jan/2006:15:04:05 -0700")).
Str("end", end.Format("02/Jan/2006:15:04:05 -0700")).Int("time_ns", int(diff)).
Msg("http")
}
type loggingResponseWriter interface {
commonLoggingResponseWriter
http.Pusher
}
func (l *responseLogger) Push(target string, opts *http.PushOptions) error {
p, ok := l.w.(http.Pusher)
if !ok {
return fmt.Errorf("responseLogger does not implement http.Pusher")
}
return p.Push(target, opts)
}
type commonLoggingResponseWriter interface {
http.ResponseWriter
http.Flusher
Status() int
Size() int
}
// responseLogger is wrapper of http.ResponseWriter that keeps track of its HTTP
// status code and body size
type responseLogger struct {
w http.ResponseWriter
status int
size int
}
func (l *responseLogger) Header() http.Header {
return l.w.Header()
}
func (l *responseLogger) Write(b []byte) (int, error) {
size, err := l.w.Write(b)
l.size += size
return size, err
}
func (l *responseLogger) WriteHeader(s int) {
l.w.WriteHeader(s)
l.status = s
}
func (l *responseLogger) Status() int {
return l.status
}
func (l *responseLogger) Size() int {
return l.size
}
func (l *responseLogger) Flush() {
f, ok := l.w.(http.Flusher)
if ok {
f.Flush()
}
}
type hijackLogger struct {
responseLogger
}
func (l *hijackLogger) Hijack() (net.Conn, *bufio.ReadWriter, error) {
h := l.responseLogger.w.(http.Hijacker)
conn, rw, err := h.Hijack()
if err == nil && l.responseLogger.status == 0 {
l.responseLogger.status = http.StatusSwitchingProtocols
}
return conn, rw, err
}
type closeNotifyWriter struct {
loggingResponseWriter
http.CloseNotifier
}
type hijackCloseNotifier struct {
loggingResponseWriter
http.Hijacker
http.CloseNotifier
}
@@ -0,0 +1,69 @@
// Copyright 2018-2021 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 prometheus
import (
"net/http"
"github.com/opencloud-eu/reva/v2/pkg/rhttp/global"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
)
const (
defaultPriority = 100
)
func init() {
global.RegisterMiddleware("prometheus", New)
}
// New returns a new HTTP middleware that counts requests for prometheus metrics
func New(m map[string]interface{}) (global.Middleware, int, error) {
namespace := m["namespace"].(string)
if namespace == "" {
namespace = "reva"
}
subsystem := m["subsystem"].(string)
ph := prometheusHandler{
counter: promauto.NewCounter(prometheus.CounterOpts{
Namespace: namespace,
Subsystem: m["subsystem"].(string),
Name: "http_requests_total",
Help: "The total number of processed " + subsystem + " HTTP requests for " + namespace,
}),
}
return ph.handler, defaultPriority, nil
}
type prometheusHandler struct {
h http.Handler
counter prometheus.Counter
}
// handler is a logging middleware
func (ph prometheusHandler) handler(h http.Handler) http.Handler {
ph.h = h
return ph
}
func (ph prometheusHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ph.h.ServeHTTP(w, r)
ph.counter.Inc()
}
@@ -0,0 +1,114 @@
// Copyright 2018-2021 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 providerauthorizer
import (
"fmt"
"net/http"
"net/url"
"strings"
ocmprovider "github.com/cs3org/go-cs3apis/cs3/ocm/provider/v1beta1"
"github.com/mitchellh/mapstructure"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
"github.com/opencloud-eu/reva/v2/pkg/ocm/provider"
"github.com/opencloud-eu/reva/v2/pkg/ocm/provider/authorizer/registry"
"github.com/opencloud-eu/reva/v2/pkg/rhttp/global"
"github.com/opencloud-eu/reva/v2/pkg/rhttp/router"
"github.com/opencloud-eu/reva/v2/pkg/utils"
)
type config struct {
Driver string `mapstructure:"driver"`
Drivers map[string]map[string]interface{} `mapstructure:"drivers"`
}
func (c *config) init() {
if c.Driver == "" {
c.Driver = "json"
}
}
func getDriver(c *config) (provider.Authorizer, error) {
if f, ok := registry.NewFuncs[c.Driver]; ok {
return f(c.Drivers[c.Driver])
}
return nil, fmt.Errorf("driver %s not found for provider authorizer", c.Driver)
}
// New returns a new HTTP middleware that verifies that the provider is registered in OCM.
func New(m map[string]interface{}, unprotected []string, ocmPrefix string) (global.Middleware, error) {
if ocmPrefix == "" {
ocmPrefix = "ocm"
}
conf := &config{}
if err := mapstructure.Decode(m, conf); err != nil {
return nil, err
}
conf.init()
authorizer, err := getDriver(conf)
if err != nil {
return nil, err
}
handler := func(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
log := appctx.GetLogger(ctx)
head, _ := router.ShiftPath(r.URL.Path)
if r.Method == "OPTIONS" || head != ocmPrefix || utils.Skip(r.URL.Path, unprotected) {
log.Info().Msg("skipping provider authorizer check for: " + r.URL.Path)
h.ServeHTTP(w, r)
return
}
userIdp := ctxpkg.ContextMustGetUser(ctx).Id.Idp
if !(strings.Contains(userIdp, "://")) {
userIdp = "https://" + userIdp
}
userIdpURL, err := url.Parse(userIdp)
if err != nil {
log.Error().Err(err).Msg("error parsing user idp in provider authorizer")
w.WriteHeader(http.StatusUnauthorized)
return
}
err = authorizer.IsProviderAllowed(ctx, &ocmprovider.ProviderInfo{
Domain: userIdpURL.Hostname(),
})
if err != nil {
log.Error().Err(err).Msg("provider not registered in OCM")
w.WriteHeader(http.StatusUnauthorized)
return
}
h.ServeHTTP(w, r)
})
}
return handler, nil
}
@@ -0,0 +1,54 @@
// Copyright 2018-2021 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 requestid
import (
"net/http"
"github.com/go-chi/chi/v5/middleware"
"github.com/opencloud-eu/reva/v2/pkg/rhttp/global"
)
const (
defaultPriority = 100
)
func init() {
global.RegisterMiddleware("requestid", New)
}
// New returns a new HTTP middleware that adds the X-Request-ID to the context
func New(m map[string]interface{}) (global.Middleware, int, error) {
rh := requestIDHandler{}
return rh.handler, defaultPriority, nil
}
type requestIDHandler struct {
h http.Handler
}
// handler is a request id middleware
func (rh requestIDHandler) handler(h http.Handler) http.Handler {
rh.h = middleware.RequestID(h)
return rh
}
func (rh requestIDHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
rh.h.ServeHTTP(w, r)
}
@@ -0,0 +1,600 @@
// Copyright 2018-2021 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 appprovider
import (
"context"
"encoding/json"
"net/http"
"net/url"
"path"
"strings"
appregistry "github.com/cs3org/go-cs3apis/cs3/app/registry/v1beta1"
providerv1beta1 "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
iso6391 "github.com/emvi/iso-639-1"
"github.com/go-chi/chi/v5"
ua "github.com/mileusna/useragent"
"github.com/mitchellh/mapstructure"
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/opencloud-eu/reva/v2/pkg/rhttp/global"
"github.com/opencloud-eu/reva/v2/pkg/sharedconf"
"github.com/opencloud-eu/reva/v2/pkg/storagespace"
"github.com/opencloud-eu/reva/v2/pkg/utils"
"github.com/pkg/errors"
"github.com/rs/zerolog"
"google.golang.org/protobuf/proto"
)
func init() {
global.Register("appprovider", New)
}
// Config holds the config options for the HTTP appprovider service
type Config struct {
Prefix string `mapstructure:"prefix"`
GatewaySvc string `mapstructure:"gatewaysvc"`
Insecure bool `mapstructure:"insecure"`
WebBaseURI string `mapstructure:"webbaseuri"`
Web Web `mapstructure:"web"`
SecureViewAppAddr string `mapstructure:"secure_view_app_addr"`
}
// Web holds the config options for the URL parameters for Web
type Web struct {
URLParamsMapping map[string]string `mapstructure:"urlparamsmapping"`
StaticURLParams map[string]string `mapstructure:"staticurlparams"`
}
func (c *Config) init() {
if c.Prefix == "" {
c.Prefix = "app"
}
c.GatewaySvc = sharedconf.GetGatewaySVC(c.GatewaySvc)
}
type svc struct {
conf *Config
router *chi.Mux
}
// New returns a new ocmd object
func New(m map[string]interface{}, log *zerolog.Logger) (global.Service, error) {
conf := &Config{}
if err := mapstructure.Decode(m, conf); err != nil {
return nil, err
}
conf.init()
r := chi.NewRouter()
s := &svc{
conf: conf,
router: r,
}
if err := s.routerInit(); err != nil {
return nil, err
}
_ = chi.Walk(s.router, func(method string, route string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error {
log.Debug().Str("service", "approvider").Str("method", method).Str("route", route).Int("middlewares", len(middlewares)).Msg("serving endpoint")
return nil
})
return s, nil
}
const (
openModeNormal = iota
openModeWeb
)
func (s *svc) routerInit() error {
s.router.Get("/list", s.handleList)
s.router.Post("/new", s.handleNew)
s.router.Post("/open", s.handleOpen(openModeNormal))
s.router.Post("/open-with-web", s.handleOpen(openModeWeb))
return nil
}
// Close performs cleanup.
func (s *svc) Close() error {
return nil
}
func (s *svc) Prefix() string {
return s.conf.Prefix
}
func (s *svc) Unprotected() []string {
return []string{"/list"}
}
func (s *svc) Handler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
s.router.ServeHTTP(w, r)
})
}
func (s *svc) handleNew(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
client, err := pool.GetGatewayServiceClient(s.conf.GatewaySvc)
if err != nil {
writeError(w, r, appErrorServerError, "error getting grpc gateway client", err)
return
}
err = r.ParseForm()
if err != nil {
writeError(w, r, appErrorInvalidParameter, "parameters could not be parsed", nil)
}
if r.Form.Get("template") != "" {
// TODO in the future we want to create a file out of the given template
writeError(w, r, appErrorUnimplemented, "template is not implemented", nil)
return
}
parentContainerIDStr := r.Form.Get("parent_container_id")
if parentContainerIDStr == "" {
writeError(w, r, appErrorInvalidParameter, "missing parent container ID", nil)
return
}
parentContainerID, err := storagespace.ParseID(parentContainerIDStr)
if err != nil {
writeError(w, r, appErrorInvalidParameter, "invalid parent container ID", nil)
return
}
filename := r.Form.Get("filename")
if filename == "" {
writeError(w, r, appErrorInvalidParameter, "missing filename", nil)
return
}
dirPart, filePart := path.Split(filename)
if dirPart != "" || filePart != filename {
writeError(w, r, appErrorInvalidParameter, "the filename must not contain a path segment", nil)
return
}
statParentContainerReq := &provider.StatRequest{
Ref: &provider.Reference{
ResourceId: &parentContainerID,
},
}
parentContainer, err := client.Stat(ctx, statParentContainerReq)
if err != nil {
writeError(w, r, appErrorServerError, "error sending a grpc stat request", err)
return
}
if parentContainer.Status.Code != rpc.Code_CODE_OK {
writeError(w, r, appErrorNotFound, "the parent container is not accessible or does not exist", err)
return
}
if parentContainer.Info.Type != provider.ResourceType_RESOURCE_TYPE_CONTAINER {
writeError(w, r, appErrorInvalidParameter, "the parent container id does not point to a container", nil)
return
}
fileRef := &provider.Reference{
ResourceId: &parentContainerID,
Path: utils.MakeRelativePath(filename),
}
statFileReq := &provider.StatRequest{
Ref: fileRef,
}
statFileRes, err := client.Stat(ctx, statFileReq)
if err != nil {
writeError(w, r, appErrorServerError, "failed to stat the file", err)
return
}
if statFileRes.Status.Code != rpc.Code_CODE_NOT_FOUND {
if statFileRes.Status.Code == rpc.Code_CODE_OK {
writeError(w, r, appErrorAlreadyExists, "the file already exists", nil)
return
}
writeError(w, r, appErrorServerError, "statting the file returned unexpected status code", err)
return
}
touchFileReq := &provider.TouchFileRequest{
Ref: fileRef,
}
touchRes, err := client.TouchFile(ctx, touchFileReq)
if err != nil {
writeError(w, r, appErrorServerError, "error sending a grpc touchfile request", err)
return
}
if touchRes.Status.Code != rpc.Code_CODE_OK {
if touchRes.Status.Code == rpc.Code_CODE_PERMISSION_DENIED {
writeError(w, r, appErrorPermissionDenied, "permission denied to create the file", nil)
return
}
writeError(w, r, appErrorServerError, "touching the file failed", nil)
return
}
// Stat the newly created file
statRes, err := client.Stat(ctx, statFileReq)
if err != nil {
writeError(w, r, appErrorServerError, "statting the created file failed", err)
return
}
if statRes.Status.Code != rpc.Code_CODE_OK {
writeError(w, r, appErrorServerError, "statting the created file failed", nil)
return
}
if statRes.Info.Type != provider.ResourceType_RESOURCE_TYPE_FILE {
writeError(w, r, appErrorInvalidParameter, "the given file id does not point to a file", nil)
return
}
fileid := storagespace.FormatResourceID(statRes.Info.Id)
js, err := json.Marshal(
map[string]interface{}{
"file_id": fileid,
},
)
if err != nil {
writeError(w, r, appErrorServerError, "error marshalling JSON response", err)
return
}
w.Header().Set("Content-Type", "application/json")
if _, err = w.Write(js); err != nil {
writeError(w, r, appErrorServerError, "error writing JSON response", err)
return
}
}
func (s *svc) handleList(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
client, err := pool.GetGatewayServiceClient(s.conf.GatewaySvc)
if err != nil {
writeError(w, r, appErrorServerError, "error getting grpc gateway client", err)
return
}
listRes, err := client.ListSupportedMimeTypes(ctx, &appregistry.ListSupportedMimeTypesRequest{})
if err != nil {
writeError(w, r, appErrorServerError, "error listing supported mime types", err)
return
}
if listRes.Status.Code != rpc.Code_CODE_OK {
writeError(w, r, appErrorServerError, "error listing supported mime types", nil)
return
}
res := buildApps(listRes.MimeTypes, r.UserAgent(), s.conf.SecureViewAppAddr)
js, err := json.Marshal(map[string]interface{}{"mime-types": res})
if err != nil {
writeError(w, r, appErrorServerError, "error marshalling JSON response", err)
return
}
w.Header().Set("Content-Type", "application/json")
if _, err = w.Write(js); err != nil {
writeError(w, r, appErrorServerError, "error writing JSON response", err)
return
}
}
func (s *svc) handleOpen(openMode int) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
client, err := pool.GetGatewayServiceClient(s.conf.GatewaySvc)
if err != nil {
writeError(w, r, appErrorServerError, "Internal error with the gateway, please try again later", err)
return
}
err = r.ParseForm()
if err != nil {
writeError(w, r, appErrorInvalidParameter, "parameters could not be parsed", nil)
}
lang := r.Form.Get("lang")
parts := strings.Split(lang, "_")
if lang != "" && !iso6391.ValidCode(parts[0]) {
writeError(w, r, appErrorInvalidParameter, "lang parameter does not contain a valid ISO 639-1 language code in the language tag", nil)
return
}
fileID := r.Form.Get("file_id")
if fileID == "" {
writeError(w, r, appErrorInvalidParameter, "missing file ID", nil)
return
}
resourceID, err := storagespace.ParseID(fileID)
if err != nil {
writeError(w, r, appErrorInvalidParameter, "invalid file ID", nil)
return
}
fileRef := &provider.Reference{
ResourceId: &resourceID,
Path: ".",
}
statRes, err := client.Stat(ctx, &provider.StatRequest{Ref: fileRef})
if err != nil {
writeError(w, r, appErrorServerError, "Internal error accessing the file, please try again later", err)
return
}
if status := utils.ReadPlainFromOpaque(statRes.GetInfo().GetOpaque(), "status"); status == "processing" {
writeError(w, r, appErrorTooEarly, "The requested file is not yet available, please try again later", nil)
return
}
viewMode, err := getViewModeFromPublicScope(ctx)
if err != nil {
writeError(w, r, appErrorPermissionDenied, "permission denied to open the application", err)
return
}
if viewMode == gateway.OpenInAppRequest_VIEW_MODE_INVALID {
// we have no publicshare Role in the token scope
// do a stat request to assemble the permissions for this user
statRes, err := client.Stat(ctx, &provider.StatRequest{Ref: fileRef})
if err != nil {
writeError(w, r, appErrorServerError, "Internal error accessing the file, please try again later", err)
return
}
if statRes.Status.Code == rpc.Code_CODE_NOT_FOUND {
writeError(w, r, appErrorNotFound, "file does not exist", nil)
return
} else if statRes.Status.Code != rpc.Code_CODE_OK {
writeError(w, r, appErrorServerError, "failed to stat the file", nil)
return
}
if statRes.Info.Type != provider.ResourceType_RESOURCE_TYPE_FILE {
writeError(w, r, appErrorInvalidParameter, "the given file id does not point to a file", nil)
return
}
// Calculate the view mode from the resource permissions
viewMode = getViewMode(statRes.Info, r.Form.Get("view_mode"))
if viewMode == gateway.OpenInAppRequest_VIEW_MODE_INVALID {
writeError(w, r, appErrorInvalidParameter, "invalid view mode", err)
return
}
}
openReq := gateway.OpenInAppRequest{
Ref: fileRef,
ViewMode: viewMode,
App: r.Form.Get("app_name"),
Opaque: utils.AppendPlainToOpaque(nil, "lang", lang),
}
templateID := r.Form.Get("template_id")
if templateID != "" {
openReq.Opaque = utils.AppendPlainToOpaque(openReq.Opaque, "template", templateID)
}
openRes, err := client.OpenInApp(ctx, &openReq)
if err != nil {
writeError(w, r, appErrorServerError,
"Error contacting the requested application, please use a different one or try again later", err)
return
}
if openRes.Status.Code != rpc.Code_CODE_OK {
if openRes.Status.Code == rpc.Code_CODE_NOT_FOUND {
writeError(w, r, appErrorNotFound, openRes.Status.Message, nil)
return
}
writeError(w, r, appErrorServerError, openRes.Status.Message,
status.NewErrorFromCode(openRes.Status.Code, "error calling OpenInApp"))
return
}
var payload interface{}
switch openMode {
case openModeNormal:
payload = openRes.AppUrl
case openModeWeb:
payload, err = newOpenInWebResponse(s.conf.WebBaseURI, s.conf.Web.URLParamsMapping, s.conf.Web.StaticURLParams, fileID, r.Form.Get("app_name"), r.Form.Get("view_mode"))
if err != nil {
writeError(w, r, appErrorServerError, "Internal error",
errors.Wrap(err, "error building OpenInWeb response"))
return
}
default:
writeError(w, r, appErrorServerError, "Internal error with the open mode",
errors.New("unknown open mode"))
return
}
js, err := json.Marshal(payload)
if err != nil {
writeError(w, r, appErrorServerError, "Internal error with JSON payload",
errors.Wrap(err, "error marshalling JSON response"))
return
}
w.Header().Set("Content-Type", "application/json")
if _, err = w.Write(js); err != nil {
writeError(w, r, appErrorServerError, "Internal error with JSON payload",
errors.Wrap(err, "error writing JSON response"))
return
}
}
}
type openInWebResponse struct {
URI string `json:"uri"`
}
func newOpenInWebResponse(baseURI string, params, staticParams map[string]string, fileID, appName, viewMode string) (openInWebResponse, error) {
uri, err := url.Parse(baseURI)
if err != nil {
return openInWebResponse{}, err
}
query := uri.Query()
for key, val := range params {
switch val {
case "fileid":
if fileID != "" {
query.Add(key, fileID)
}
case "appname":
if appName != "" {
query.Add(key, appName)
}
case "viewmode":
if viewMode != "" {
query.Add(key, viewMode)
}
default:
return openInWebResponse{}, errors.New("unknown parameter mapper")
}
}
for key, val := range staticParams {
query.Add(key, val)
}
uri.RawQuery = query.Encode()
return openInWebResponse{URI: uri.String()}, nil
}
// MimeTypeInfo wraps the appregistry.MimeTypeInfo to change the app providers to ProviderInfos with a secure view flag
type MimeTypeInfo struct {
appregistry.MimeTypeInfo
AppProviders []*ProviderInfo `json:"app_providers"`
}
// ProviderInfo wraps the appregistry.ProviderInfo to add a secure view flag
type ProviderInfo struct {
appregistry.ProviderInfo
// TODO make this part of the CS3 provider info
SecureView bool `json:"secure_view"`
TargetExt string `json:"target_ext,omitempty"`
}
// buildApps rewrites the mime type info to only include apps that
// * have a name
// * can be called by the user agent, eg Desktop-only
//
// it also
// * wraps the provider info to be able to add a secure view flag
// * adds a secure view flag if the address matches the secure view app address and
// * removes the address from the provider info to not expose internal addresses
func buildApps(mimeTypes []*appregistry.MimeTypeInfo, userAgent, secureViewAppAddr string) []*MimeTypeInfo {
ua := ua.Parse(userAgent)
res := []*MimeTypeInfo{}
for _, m := range mimeTypes {
apps := []*ProviderInfo{}
for _, p := range m.AppProviders {
ep := &ProviderInfo{}
proto.Merge(&ep.ProviderInfo, p)
if p.Address == secureViewAppAddr {
ep.SecureView = true
}
p.Address = "" // address is internal only and not needed in the client
// apps are called by name, so if it has no name it cannot be called and should not be advertised
// also filter Desktop-only apps if ua is not Desktop
if p.Name != "" && (ua.Desktop || !p.DesktopOnly) {
apps = append(apps, ep)
}
}
if len(apps) > 0 {
mt := &MimeTypeInfo{}
addTemplateInfo(m, apps)
proto.Merge(&mt.MimeTypeInfo, m)
mt.AppProviders = apps
res = append(res, mt)
}
}
return res
}
func getViewMode(res *provider.ResourceInfo, vm string) gateway.OpenInAppRequest_ViewMode {
if vm != "" {
return utils.GetViewMode(vm)
}
var viewMode gateway.OpenInAppRequest_ViewMode
canEdit := res.PermissionSet.InitiateFileUpload
canView := res.PermissionSet.InitiateFileDownload
switch {
case canEdit && canView:
viewMode = gateway.OpenInAppRequest_VIEW_MODE_READ_WRITE
case canView:
viewMode = gateway.OpenInAppRequest_VIEW_MODE_READ_ONLY
default:
viewMode = gateway.OpenInAppRequest_VIEW_MODE_INVALID
}
return viewMode
}
// try to get the view mode from a publicshare scope
func getViewModeFromPublicScope(ctx context.Context) (gateway.OpenInAppRequest_ViewMode, error) {
scopes, ok := ctxpkg.ContextGetScopes(ctx)
if ok {
for key, scope := range scopes {
if strings.HasPrefix(key, "publicshare:") {
switch scope.GetRole() {
case providerv1beta1.Role_ROLE_VIEWER:
return gateway.OpenInAppRequest_VIEW_MODE_VIEW_ONLY, nil
case providerv1beta1.Role_ROLE_EDITOR:
return gateway.OpenInAppRequest_VIEW_MODE_READ_WRITE, nil
default:
return gateway.OpenInAppRequest_VIEW_MODE_INVALID, errors.New("invalid view mode in publicshare scope")
}
}
}
}
return gateway.OpenInAppRequest_VIEW_MODE_INVALID, nil
}
@@ -0,0 +1,82 @@
// Copyright 2018-2021 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 appprovider
import (
"encoding/json"
"net/http"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
)
// appErrorCode stores the type of error encountered
type appErrorCode string
const (
appErrorNotFound appErrorCode = "RESOURCE_NOT_FOUND"
appErrorAlreadyExists appErrorCode = "RESOURCE_ALREADY_EXISTS"
appErrorUnauthenticated appErrorCode = "UNAUTHENTICATED"
appErrorPermissionDenied appErrorCode = "PERMISSION_DENIED"
appErrorUnimplemented appErrorCode = "NOT_IMPLEMENTED"
appErrorInvalidParameter appErrorCode = "INVALID_PARAMETER"
appErrorServerError appErrorCode = "SERVER_ERROR"
appErrorTooEarly appErrorCode = "TOO_EARLY"
)
// appErrorCodeMapping stores the HTTP error code mapping for various APIErrorCodes
var appErrorCodeMapping = map[appErrorCode]int{
appErrorNotFound: http.StatusNotFound,
appErrorAlreadyExists: http.StatusForbidden,
appErrorUnauthenticated: http.StatusUnauthorized,
appErrorUnimplemented: http.StatusNotImplemented,
appErrorInvalidParameter: http.StatusBadRequest,
appErrorServerError: http.StatusInternalServerError,
appErrorPermissionDenied: http.StatusForbidden,
appErrorTooEarly: http.StatusTooEarly,
}
// APIError encompasses the error type and message
type appError struct {
Code appErrorCode `json:"code"`
Message string `json:"message"`
}
// writeError handles writing error responses
func writeError(w http.ResponseWriter, r *http.Request, code appErrorCode, message string, err error) {
if err != nil {
appctx.GetLogger(r.Context()).Error().Err(err).Msg(message)
}
var encoded []byte
w.Header().Set("Content-Type", "application/json")
encoded, err = json.MarshalIndent(appError{Code: code, Message: message}, "", " ")
if err != nil {
appctx.GetLogger(r.Context()).Error().Err(err).Msg("error encoding response")
w.WriteHeader(http.StatusInternalServerError)
return
}
w.WriteHeader(appErrorCodeMapping[code])
_, err = w.Write(encoded)
if err != nil {
appctx.GetLogger(r.Context()).Error().Err(err).Msg("error writing response")
w.WriteHeader(http.StatusInternalServerError)
}
}
@@ -0,0 +1,90 @@
package appprovider
import (
"strings"
appregistry "github.com/cs3org/go-cs3apis/cs3/app/registry/v1beta1"
)
type TemplateList struct {
Templates map[string][]Template `json:"templates"`
}
type Template struct {
Extension string `json:"extension"`
MimeType string `json:"mime_type"`
TargetExtension string `json:"target_extension"`
}
var tl = TemplateList{
Templates: map[string][]Template{
"collabora": {
{
MimeType: "application/vnd.oasis.opendocument.spreadsheet-template",
TargetExtension: "ods",
},
{
MimeType: "application/vnd.oasis.opendocument.text-template",
TargetExtension: "odt",
},
{
MimeType: "application/vnd.oasis.opendocument.presentation-template",
TargetExtension: "odp",
},
},
"onlyoffice": {
{
MimeType: "application/vnd.ms-word.template.macroenabled.12",
TargetExtension: "docx",
},
{
MimeType: "application/vnd.oasis.opendocument.text-template",
TargetExtension: "docx",
},
{
MimeType: "application/vnd.openxmlformats-officedocument.wordprocessingml.template",
TargetExtension: "docx",
},
{
MimeType: "application/vnd.oasis.opendocument.spreadsheet-template",
TargetExtension: "xlsx",
},
{
MimeType: "application/vnd.ms-excel.template.macroenabled.12",
TargetExtension: "xlsx",
},
{
MimeType: "application/vnd.openxmlformats-officedocument.spreadsheetml.template",
TargetExtension: "xlsx",
},
{
MimeType: "application/vnd.oasis.opendocument.presentation-template",
TargetExtension: "pptx",
},
{
MimeType: "application/vnd.ms-powerpoint.template.macroenabled.12",
TargetExtension: "pptx",
},
{
MimeType: "application/vnd.openxmlformats-officedocument.presentationml.template",
TargetExtension: "pptx",
},
},
},
}
func addTemplateInfo(mt *appregistry.MimeTypeInfo, apps []*ProviderInfo) {
for _, app := range apps {
if tls, ok := tl.Templates[strings.ToLower(app.ProductName)]; ok {
for _, tmpl := range tls {
if tmpl.Extension != "" && tmpl.Extension == mt.Ext {
app.TargetExt = tmpl.TargetExtension
continue
}
if tmpl.MimeType == mt.MimeType {
app.TargetExt = tmpl.TargetExtension
}
}
}
}
}
@@ -0,0 +1,293 @@
// Copyright 2018-2021 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 archiver
import (
"context"
"errors"
"fmt"
"net/http"
"time"
"regexp"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/gdexlab/go-render/render"
"github.com/mitchellh/mapstructure"
"github.com/opencloud-eu/reva/v2/internal/http/services/archiver/manager"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/opencloud-eu/reva/v2/pkg/rhttp"
"github.com/opencloud-eu/reva/v2/pkg/rhttp/global"
"github.com/opencloud-eu/reva/v2/pkg/sharedconf"
"github.com/opencloud-eu/reva/v2/pkg/storage/utils/downloader"
"github.com/opencloud-eu/reva/v2/pkg/storage/utils/walker"
"github.com/opencloud-eu/reva/v2/pkg/storagespace"
"github.com/rs/zerolog"
)
type svc struct {
config *Config
gatewaySelector pool.Selectable[gateway.GatewayAPIClient]
log *zerolog.Logger
walker walker.Walker
downloader downloader.Downloader
allowedFolders []*regexp.Regexp
}
// Config holds the config options that need to be passed down to all ocdav handlers
type Config struct {
Prefix string `mapstructure:"prefix"`
GatewaySvc string `mapstructure:"gatewaysvc"`
Timeout int64 `mapstructure:"timeout"`
Insecure bool `mapstructure:"insecure"`
Name string `mapstructure:"name"`
MaxNumFiles int64 `mapstructure:"max_num_files"`
MaxSize int64 `mapstructure:"max_size"`
AllowedFolders []string `mapstructure:"allowed_folders"`
}
func init() {
global.Register("archiver", New)
}
// New creates a new archiver service
func New(conf map[string]interface{}, log *zerolog.Logger) (global.Service, error) {
c := &Config{}
err := mapstructure.Decode(conf, c)
if err != nil {
return nil, err
}
c.init()
gatewaySelector, err := pool.GatewaySelector(c.GatewaySvc)
if err != nil {
return nil, err
}
// compile all the regex for filtering folders
allowedFolderRegex := make([]*regexp.Regexp, 0, len(c.AllowedFolders))
for _, s := range c.AllowedFolders {
regex, err := regexp.Compile(s)
if err != nil {
return nil, err
}
allowedFolderRegex = append(allowedFolderRegex, regex)
}
return &svc{
config: c,
gatewaySelector: gatewaySelector,
downloader: downloader.NewDownloader(gatewaySelector, rhttp.Insecure(c.Insecure), rhttp.Timeout(time.Duration(c.Timeout*int64(time.Second)))),
walker: walker.NewWalker(gatewaySelector),
log: log,
allowedFolders: allowedFolderRegex,
}, nil
}
func (c *Config) init() {
if c.Prefix == "" {
c.Prefix = "download_archive"
}
if c.Name == "" {
c.Name = "download"
}
c.GatewaySvc = sharedconf.GetGatewaySVC(c.GatewaySvc)
}
func (s *svc) getResources(ctx context.Context, paths, ids []string) ([]*provider.ResourceId, error) {
if len(paths) == 0 && len(ids) == 0 {
return nil, errtypes.BadRequest("path and id lists are both empty")
}
resources := make([]*provider.ResourceId, 0, len(paths)+len(ids))
for _, id := range ids {
// id is base64 encoded and after decoding has the form <storage_id>:<resource_id>
decodedID, err := storagespace.ParseID(id)
if err != nil {
return nil, errors.New("could not unwrap given file id")
}
resources = append(resources, &decodedID)
}
gatewayClient, err := s.gatewaySelector.Next()
if err != nil {
return nil, err
}
for _, p := range paths {
// id is base64 encoded and after decoding has the form <storage_id>:<resource_id>
resp, err := gatewayClient.Stat(ctx, &provider.StatRequest{
Ref: &provider.Reference{
Path: p,
},
})
switch {
case err != nil:
return nil, err
case resp.Status.Code == rpc.Code_CODE_NOT_FOUND:
return nil, errtypes.NotFound(p)
case resp.Status.Code != rpc.Code_CODE_OK:
return nil, errtypes.InternalError(fmt.Sprintf("error stating %s", p))
}
resources = append(resources, resp.Info.Id)
}
// check if all the folders are allowed to be archived
/* FIXME bring back filtering
err := s.allAllowed(resources)
if err != nil {
return nil, err
}
*/
return resources, nil
}
// return true if path match with at least with one allowed folder regex
/*
func (s *svc) isPathAllowed(path string) bool {
for _, reg := range s.allowedFolders {
if reg.MatchString(path) {
return true
}
}
return false
}
// return nil if all the paths in the slide match with at least one allowed folder regex
func (s *svc) allAllowed(paths []string) error {
if len(s.allowedFolders) == 0 {
return nil
}
for _, f := range paths {
if !s.isPathAllowed(f) {
return errtypes.BadRequest(fmt.Sprintf("resource at %s not allowed to be archived", f))
}
}
return nil
}
*/
func (s *svc) writeHTTPError(rw http.ResponseWriter, err error) {
s.log.Error().Msg(err.Error())
switch err.(type) {
case errtypes.NotFound, errtypes.PermissionDenied:
rw.WriteHeader(http.StatusNotFound)
case manager.ErrMaxSize, manager.ErrMaxFileCount:
rw.WriteHeader(http.StatusRequestEntityTooLarge)
case errtypes.BadRequest:
rw.WriteHeader(http.StatusBadRequest)
default:
rw.WriteHeader(http.StatusInternalServerError)
}
_, _ = rw.Write([]byte(err.Error()))
}
func (s *svc) Handler() http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
// get the paths and/or the resources id from the query
ctx := r.Context()
v := r.URL.Query()
paths, ok := v["path"]
if !ok {
paths = []string{}
}
ids, ok := v["id"]
if !ok {
ids = []string{}
}
format := v.Get("output-format")
if format == "" {
format = "zip"
}
resources, err := s.getResources(ctx, paths, ids)
if err != nil {
s.writeHTTPError(rw, err)
return
}
arch, err := manager.NewArchiver(resources, s.walker, s.downloader, manager.Config{
MaxNumFiles: s.config.MaxNumFiles,
MaxSize: s.config.MaxSize,
})
if err != nil {
s.writeHTTPError(rw, err)
return
}
archName := s.config.Name
if format == "tar" {
archName += ".tar"
} else {
archName += ".zip"
}
s.log.Debug().Msg("Requested the following resources to archive: " + render.Render(resources))
rw.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", archName))
rw.Header().Set("Content-Transfer-Encoding", "binary")
// create the archive
var closeArchive func()
if format == "tar" {
closeArchive, err = arch.CreateTar(ctx, rw)
} else {
closeArchive, err = arch.CreateZip(ctx, rw)
}
defer closeArchive()
if err != nil {
s.writeHTTPError(rw, err)
return
}
})
}
func (s *svc) Prefix() string {
return s.config.Prefix
}
func (s *svc) Close() error {
return nil
}
func (s *svc) Unprotected() []string {
return nil
}
@@ -0,0 +1,208 @@
// Copyright 2018-2021 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 manager
import (
"archive/tar"
"archive/zip"
"context"
"io"
"path/filepath"
"time"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/opencloud-eu/reva/v2/pkg/storage/utils/downloader"
"github.com/opencloud-eu/reva/v2/pkg/storage/utils/walker"
"github.com/opencloud-eu/reva/v2/pkg/utils"
)
// Config is the config for the Archiver
type Config struct {
MaxNumFiles int64
MaxSize int64
}
// Archiver is the struct able to create an archive
type Archiver struct {
resources []*provider.ResourceId
walker walker.Walker
downloader downloader.Downloader
config Config
}
// NewArchiver creates a new archiver able to create an archive containing the files in the list
func NewArchiver(r []*provider.ResourceId, w walker.Walker, d downloader.Downloader, config Config) (*Archiver, error) {
if len(r) == 0 {
return nil, ErrEmptyList{}
}
arc := &Archiver{
resources: r,
walker: w,
downloader: d,
config: config,
}
return arc, nil
}
// CreateTar creates a tar and write it into the dst Writer
func (a *Archiver) CreateTar(ctx context.Context, dst io.Writer) (func(), error) {
w := tar.NewWriter(dst)
closer := func() {
_ = w.Close()
}
var filesCount, sizeFiles int64
for _, root := range a.resources {
err := a.walker.Walk(ctx, root, func(wd string, info *provider.ResourceInfo, err error) error {
if err != nil {
return err
}
// when archiving a space we can omit the spaceroot
if utils.IsSpaceRoot(info) {
return nil
}
isDir := info.Type == provider.ResourceType_RESOURCE_TYPE_CONTAINER
filesCount++
if filesCount > a.config.MaxNumFiles {
return ErrMaxFileCount{}
}
if !isDir {
// only add the size if the resource is not a directory
// as its size could be resursive-computed, and we would
// count the files not only once
sizeFiles += int64(info.Size)
if sizeFiles > a.config.MaxSize {
return ErrMaxSize{}
}
}
header := tar.Header{
Name: filepath.Join(wd, info.Path),
ModTime: time.Unix(int64(info.Mtime.Seconds), 0),
}
if isDir {
// the resource is a folder
header.Mode = 0755
header.Typeflag = tar.TypeDir
} else {
header.Mode = 0644
header.Typeflag = tar.TypeReg
header.Size = int64(info.Size)
}
err = w.WriteHeader(&header)
if err != nil {
return err
}
if !isDir {
err = a.downloader.Download(ctx, info.Id, w)
if err != nil {
return err
}
}
return nil
})
if err != nil {
return closer, err
}
}
return closer, nil
}
// CreateZip creates a zip and write it into the dst Writer
func (a *Archiver) CreateZip(ctx context.Context, dst io.Writer) (func(), error) {
w := zip.NewWriter(dst)
closer := func() {
_ = w.Close()
}
var filesCount, sizeFiles int64
for _, root := range a.resources {
err := a.walker.Walk(ctx, root, func(wd string, info *provider.ResourceInfo, err error) error {
if err != nil {
return err
}
// when archiving a space we can omit the spaceroot
if utils.IsSpaceRoot(info) {
return nil
}
isDir := info.Type == provider.ResourceType_RESOURCE_TYPE_CONTAINER
filesCount++
if filesCount > a.config.MaxNumFiles {
return ErrMaxFileCount{}
}
if !isDir {
// only add the size if the resource is not a directory
// as its size could be resursive-computed, and we would
// count the files not only once
sizeFiles += int64(info.Size)
if sizeFiles > a.config.MaxSize {
return ErrMaxSize{}
}
}
header := zip.FileHeader{
Name: filepath.Join(wd, info.Path),
Modified: time.Unix(int64(info.Mtime.Seconds), 0),
}
if isDir {
header.Name += "/"
} else {
header.UncompressedSize64 = info.Size
}
dst, err := w.CreateHeader(&header)
if err != nil {
return err
}
if !isDir {
err = a.downloader.Download(ctx, info.Id, dst)
if err != nil {
return err
}
}
return nil
})
if err != nil {
return closer, err
}
}
return closer, nil
}
@@ -0,0 +1,43 @@
// Copyright 2018-2021 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 manager
// ErrMaxFileCount is the error returned when the max files count specified in the config has reached
type ErrMaxFileCount struct{}
// ErrMaxSize is the error returned when the max total files size specified in the config has reached
type ErrMaxSize struct{}
// ErrEmptyList is the error returned when an empty list is passed when an archiver is created
type ErrEmptyList struct{}
// Error returns the string error msg for ErrMaxFileCount
func (ErrMaxFileCount) Error() string {
return "reached max files count"
}
// Error returns the string error msg for ErrMaxSize
func (ErrMaxSize) Error() string {
return "reached max total files size"
}
// Error returns the string error msg for ErrEmptyList
func (ErrEmptyList) Error() string {
return "list of files to archive empty"
}
@@ -0,0 +1,237 @@
// Copyright 2018-2021 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 datagateway
import (
"context"
"io"
"net/http"
"net/url"
"path"
"strconv"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/mitchellh/mapstructure"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/rhttp"
"github.com/opencloud-eu/reva/v2/pkg/rhttp/global"
"github.com/opencloud-eu/reva/v2/pkg/sharedconf"
"github.com/pkg/errors"
"github.com/rs/zerolog"
"go.opentelemetry.io/otel"
semconv "go.opentelemetry.io/otel/semconv/v1.20.0"
"go.opentelemetry.io/otel/trace"
)
var tracer trace.Tracer
func init() {
tracer = otel.Tracer("github.com/opencloud-eu/reva/v2/pkg/storage/utils/decomposedfs/tree")
}
const (
// TokenTransportHeader holds the header key for the reva transfer token
TokenTransportHeader = "X-Reva-Transfer"
)
func init() {
global.Register("datagateway", New)
}
// transferClaims are custom claims for a JWT token to be used between the metadata and data gateways.
type transferClaims struct {
jwt.RegisteredClaims
Target string `json:"target"`
}
type config struct {
Prefix string `mapstructure:"prefix"`
TransferSharedSecret string `mapstructure:"transfer_shared_secret"`
Timeout int64 `mapstructure:"timeout"`
Insecure bool `mapstructure:"insecure"`
}
func (c *config) init() {
if c.Prefix == "" {
c.Prefix = "datagateway"
}
c.TransferSharedSecret = sharedconf.GetJWTSecret(c.TransferSharedSecret)
}
type svc struct {
conf *config
handler http.Handler
client *http.Client
}
// New returns a new datagateway
func New(m map[string]interface{}, log *zerolog.Logger) (global.Service, error) {
conf := &config{}
if err := mapstructure.Decode(m, conf); err != nil {
return nil, err
}
conf.init()
s := &svc{
conf: conf,
client: rhttp.GetHTTPClient(
rhttp.Timeout(time.Duration(conf.Timeout*int64(time.Second))),
rhttp.Insecure(conf.Insecure),
),
}
s.setHandler()
return s, nil
}
// Close performs cleanup.
func (s *svc) Close() error {
return nil
}
func (s *svc) Prefix() string {
return s.conf.Prefix
}
func (s *svc) Handler() http.Handler {
return s.handler
}
func (s *svc) Unprotected() []string {
return []string{
"/",
}
}
func (s *svc) setHandler() {
s.handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
ctx, span := tracer.Start(ctx, "HandlerFunc")
defer span.End()
span.SetAttributes(
semconv.HTTPMethodKey.String(r.Method),
semconv.HTTPURLKey.String(r.URL.String()),
)
r = r.WithContext(ctx)
s.doRequest(w, r)
})
}
// verify extracts the transfer token from the request
// If it is not set as header we assume that it's the last path segment instead.
func (s *svc) verify(ctx context.Context, r *http.Request) (*transferClaims, error) {
token := r.Header.Get(TokenTransportHeader)
if token == "" {
token = path.Base(r.URL.Path)
r.Header.Set(TokenTransportHeader, token)
}
j, err := jwt.ParseWithClaims(token, &transferClaims{}, func(token *jwt.Token) (interface{}, error) {
return []byte(s.conf.TransferSharedSecret), nil
})
if err != nil {
return nil, errors.Wrap(err, "error parsing token")
}
if claims, ok := j.Claims.(*transferClaims); ok && j.Valid {
return claims, nil
}
err = errtypes.InvalidCredentials("token invalid")
return nil, err
}
func (s *svc) doRequest(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
log := appctx.GetLogger(ctx)
claims, err := s.verify(ctx, r)
if err != nil {
err = errors.Wrap(err, "datagateway: error validating transfer token")
log.Err(err).Str("token", r.Header.Get(TokenTransportHeader)).Msg("invalid transfer token")
w.WriteHeader(http.StatusForbidden)
return
}
target := claims.Target
// add query params to target, clients can send checksums and other information.
targetURL, err := url.Parse(target)
if err != nil {
log.Err(err).Msg("datagateway: error parsing target url")
w.WriteHeader(http.StatusInternalServerError)
return
}
targetURL.RawQuery = r.URL.RawQuery
target = targetURL.String()
log.Debug().Str("target", target).Msg("sending request to internal data server")
httpReq, err := rhttp.NewRequest(ctx, r.Method, target, r.Body)
if err != nil {
log.Err(err).Msg("wrong request")
w.WriteHeader(http.StatusInternalServerError)
return
}
httpReq.Header = r.Header
httpReq.ContentLength = r.ContentLength
httpRes, err := s.client.Do(httpReq)
if err != nil {
log.Err(err).Msg("error doing " + r.Method + " request to data service")
w.WriteHeader(http.StatusInternalServerError)
return
}
defer httpRes.Body.Close()
copyHeader(w.Header(), httpRes.Header)
if httpRes.StatusCode != http.StatusOK && httpRes.StatusCode != http.StatusPartialContent {
// swallow the body and set content-length to 0 to prevent reverse proxies from trying to read from it
w.Header().Set("Content-Length", "0")
w.WriteHeader(httpRes.StatusCode)
return
}
w.WriteHeader(httpRes.StatusCode)
var c int64
c, err = io.Copy(w, httpRes.Body)
if err != nil {
log.Err(err).Msg("error writing body after header were set")
}
if httpRes.Header.Get("Content-Length") != "" {
i, err := strconv.ParseInt(httpRes.Header.Get("Content-Length"), 10, 64)
if err != nil {
log.Error().Err(err).Str("content-length", httpRes.Header.Get("Content-Length")).Msg("invalid content length in dataprovider response")
}
if i != c {
log.Error().Int64("content-length", i).Int64("transferred-bytes", c).Msg("content length vs transferred bytes mismatch")
}
}
}
func copyHeader(dst, src http.Header) {
for key, values := range src {
for i := range values {
dst.Set(key, values[i])
}
}
}
@@ -0,0 +1,201 @@
// Copyright 2018-2021 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 dataprovider
import (
"fmt"
"net/http"
"github.com/mitchellh/mapstructure"
"github.com/rs/zerolog"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
"github.com/opencloud-eu/reva/v2/pkg/events"
"github.com/opencloud-eu/reva/v2/pkg/events/stream"
datatxregistry "github.com/opencloud-eu/reva/v2/pkg/rhttp/datatx/manager/registry"
"github.com/opencloud-eu/reva/v2/pkg/rhttp/global"
"github.com/opencloud-eu/reva/v2/pkg/rhttp/router"
"github.com/opencloud-eu/reva/v2/pkg/storage"
"github.com/opencloud-eu/reva/v2/pkg/storage/fs/registry"
)
func init() {
global.Register("dataprovider", New)
}
type config struct {
Prefix string `mapstructure:"prefix" docs:"data;The prefix to be used for this HTTP service"`
Driver string `mapstructure:"driver" docs:"localhome;The storage driver to be used."`
Drivers map[string]map[string]interface{} `mapstructure:"drivers" docs:"url:pkg/storage/fs/localhome/localhome.go;The configuration for the storage driver"`
DataTXs map[string]map[string]interface{} `mapstructure:"data_txs" docs:"url:pkg/rhttp/datatx/manager/simple/simple.go;The configuration for the data tx protocols"`
NatsAddress string `mapstructure:"nats_address"`
NatsClusterID string `mapstructure:"nats_clusterID"`
NatsTLSInsecure bool `mapstructure:"nats_tls_insecure"`
NatsRootCACertPath string `mapstructure:"nats_root_ca_cert_path"`
NatsEnableTLS bool `mapstructure:"nats_enable_tls"`
NatsUsername string `mapstructure:"nats_username"`
NatsPassword string `mapstructure:"nats_password"`
}
func (c *config) init() {
if c.Prefix == "" {
c.Prefix = "data"
}
if c.Driver == "" {
c.Driver = "localhome"
}
}
type svc struct {
conf *config
handler http.Handler
storage storage.FS
dataTXs map[string]http.Handler
}
// New returns a new datasvc
func New(m map[string]interface{}, log *zerolog.Logger) (global.Service, error) {
conf := &config{}
if err := mapstructure.Decode(m, conf); err != nil {
return nil, err
}
conf.init()
var evstream events.Stream
if conf.NatsAddress == "" || conf.NatsClusterID == "" {
log.Warn().Msg("missing or incomplete nats configuration. Events will not be published.")
} else {
s, err := stream.NatsFromConfig("dataprovider", false, stream.NatsConfig{
Endpoint: conf.NatsAddress,
Cluster: conf.NatsClusterID,
EnableTLS: conf.NatsEnableTLS,
TLSInsecure: conf.NatsTLSInsecure,
TLSRootCACertificate: conf.NatsRootCACertPath,
AuthUsername: conf.NatsUsername,
AuthPassword: conf.NatsPassword,
})
if err != nil {
return nil, err
}
evstream = s
}
fs, err := getFS(conf, evstream, log)
if err != nil {
return nil, err
}
dataTXs, err := getDataTXs(conf, fs, evstream, log)
if err != nil {
return nil, err
}
s := &svc{
storage: fs,
conf: conf,
dataTXs: dataTXs,
}
err = s.setHandler()
return s, err
}
func getFS(c *config, stream events.Stream, log *zerolog.Logger) (storage.FS, error) {
if f, ok := registry.NewFuncs[c.Driver]; ok {
return f(c.Drivers[c.Driver], stream, log)
}
return nil, fmt.Errorf("driver not found: %s", c.Driver)
}
func getDataTXs(c *config, fs storage.FS, publisher events.Publisher, log *zerolog.Logger) (map[string]http.Handler, error) {
if c.DataTXs == nil {
c.DataTXs = make(map[string]map[string]interface{})
}
if len(c.DataTXs) == 0 {
c.DataTXs["simple"] = make(map[string]interface{})
c.DataTXs["simple"]["cache_store"] = "noop"
c.DataTXs["simple"]["cache_database"] = "reva"
c.DataTXs["spaces"] = make(map[string]interface{})
c.DataTXs["spaces"]["cache_store"] = "noop"
c.DataTXs["spaces"]["cache_database"] = "reva"
c.DataTXs["tus"] = make(map[string]interface{})
c.DataTXs["tus"]["cache_store"] = "noop"
c.DataTXs["tus"]["cache_database"] = "reva"
}
txs := make(map[string]http.Handler)
for t := range c.DataTXs {
if f, ok := datatxregistry.NewFuncs[t]; ok {
if tx, err := f(c.DataTXs[t], publisher, log); err == nil {
if handler, err := tx.Handler(fs); err == nil {
txs[t] = handler
}
}
}
}
return txs, nil
}
func (s *svc) Close() error {
return nil
}
func (s *svc) Unprotected() []string {
return []string{
"/tus",
}
}
func (s *svc) Prefix() string {
return s.conf.Prefix
}
func (s *svc) Handler() http.Handler {
return s.handler
}
func (s *svc) setHandler() error {
s.handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log := appctx.GetLogger(r.Context())
log.Debug().Msgf("dataprovider routing: path=%s", r.URL.Path)
head, tail := router.ShiftPath(r.URL.Path)
if handler, ok := s.dataTXs[head]; ok {
r.URL.Path = tail
handler.ServeHTTP(w, r)
return
}
// If we don't find a prefix match for any of the protocols, upload the resource
// through the direct HTTP protocol
if handler, ok := s.dataTXs["simple"]; ok {
handler.ServeHTTP(w, r)
return
}
w.WriteHeader(http.StatusInternalServerError)
})
return nil
}
@@ -0,0 +1,85 @@
// Copyright 2018-2021 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 helloworld
import (
"net/http"
"github.com/mitchellh/mapstructure"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
"github.com/opencloud-eu/reva/v2/pkg/rhttp/global"
"github.com/rs/zerolog"
)
func init() {
global.Register("helloworld", New)
}
// New returns a new helloworld service
func New(m map[string]interface{}, log *zerolog.Logger) (global.Service, error) {
conf := &config{}
if err := mapstructure.Decode(m, conf); err != nil {
return nil, err
}
conf.init()
return &svc{conf: conf}, nil
}
// Close performs cleanup.
func (s *svc) Close() error {
return nil
}
type config struct {
Prefix string `mapstructure:"prefix"`
HelloMessage string `mapstructure:"message"`
}
func (c *config) init() {
if c.HelloMessage == "" {
c.HelloMessage = "Hello World!"
}
if c.Prefix == "" {
c.Prefix = "helloworld"
}
}
type svc struct {
conf *config
}
func (s *svc) Prefix() string {
return s.conf.Prefix
}
func (s *svc) Unprotected() []string {
return []string{"/"}
}
func (s *svc) Handler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log := appctx.GetLogger(r.Context())
if _, err := w.Write([]byte(s.conf.HelloMessage)); err != nil {
log.Err(err).Msg("error writing response")
}
})
}
@@ -0,0 +1,41 @@
// Copyright 2018-2021 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 loader
import (
// Load core HTTP services
_ "github.com/opencloud-eu/reva/v2/internal/http/services/appprovider"
_ "github.com/opencloud-eu/reva/v2/internal/http/services/archiver"
_ "github.com/opencloud-eu/reva/v2/internal/http/services/datagateway"
_ "github.com/opencloud-eu/reva/v2/internal/http/services/dataprovider"
_ "github.com/opencloud-eu/reva/v2/internal/http/services/helloworld"
_ "github.com/opencloud-eu/reva/v2/internal/http/services/mentix"
_ "github.com/opencloud-eu/reva/v2/internal/http/services/metrics"
_ "github.com/opencloud-eu/reva/v2/internal/http/services/ocmd"
_ "github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav"
_ "github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocs"
_ "github.com/opencloud-eu/reva/v2/internal/http/services/preferences"
_ "github.com/opencloud-eu/reva/v2/internal/http/services/prometheus"
_ "github.com/opencloud-eu/reva/v2/internal/http/services/reverseproxy"
_ "github.com/opencloud-eu/reva/v2/internal/http/services/sciencemesh"
_ "github.com/opencloud-eu/reva/v2/internal/http/services/siteacc"
_ "github.com/opencloud-eu/reva/v2/internal/http/services/sysinfo"
_ "github.com/opencloud-eu/reva/v2/internal/http/services/wellknown"
// Add your own service here
)
@@ -0,0 +1,194 @@
// Copyright 2018-2021 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 mentix
import (
"net/http"
"github.com/mitchellh/mapstructure"
"github.com/opencloud-eu/reva/v2/pkg/mentix/meshdata"
"github.com/pkg/errors"
"github.com/rs/zerolog"
"github.com/opencloud-eu/reva/v2/pkg/mentix"
"github.com/opencloud-eu/reva/v2/pkg/mentix/config"
"github.com/opencloud-eu/reva/v2/pkg/mentix/exchangers"
"github.com/opencloud-eu/reva/v2/pkg/rhttp/global"
)
func init() {
global.Register(serviceName, New)
}
type svc struct {
conf *config.Configuration
mntx *mentix.Mentix
log *zerolog.Logger
stopSignal chan struct{}
}
const (
serviceName = "mentix"
)
func (s *svc) Close() error {
// Trigger and close the stopSignal signal channel to stop Mentix
s.stopSignal <- struct{}{}
close(s.stopSignal)
return nil
}
func (s *svc) Prefix() string {
return s.conf.Prefix
}
func (s *svc) Unprotected() []string {
// Get all endpoints exposed by the RequestExchangers
importers := s.mntx.GetRequestImporters()
exporters := s.mntx.GetRequestExporters()
getEndpoints := func(exchangers []exchangers.RequestExchanger) []string {
endpoints := make([]string, 0, len(exchangers))
for _, exchanger := range exchangers {
if !exchanger.IsProtectedEndpoint() {
endpoints = append(endpoints, exchanger.Endpoint())
}
}
return endpoints
}
endpoints := make([]string, 0, len(importers)+len(exporters))
endpoints = append(endpoints, getEndpoints(importers)...)
endpoints = append(endpoints, getEndpoints(exporters)...)
return endpoints
}
func (s *svc) Handler() http.Handler {
// Forward requests to Mentix
return http.HandlerFunc(s.mntx.RequestHandler)
}
func (s *svc) startBackgroundService() {
// Just run Mentix in the background
go func() {
if err := s.mntx.Run(s.stopSignal); err != nil {
s.log.Err(err).Msg("error while running mentix")
}
}()
}
func parseConfig(m map[string]interface{}) (*config.Configuration, error) {
cfg := &config.Configuration{}
if err := mapstructure.Decode(m, &cfg); err != nil {
return nil, errors.Wrap(err, "mentix: error decoding configuration")
}
applyInternalConfig(m, cfg)
applyDefaultConfig(cfg)
return cfg, nil
}
func applyInternalConfig(m map[string]interface{}, conf *config.Configuration) {
getSubsections := func(section string) []string {
subsections := make([]string, 0, 5)
if list, ok := m[section].(map[string]interface{}); ok {
for name := range list {
subsections = append(subsections, name)
}
}
return subsections
}
conf.EnabledConnectors = getSubsections("connectors")
conf.EnabledImporters = getSubsections("importers")
conf.EnabledExporters = getSubsections("exporters")
}
func applyDefaultConfig(conf *config.Configuration) {
// General
if conf.Prefix == "" {
conf.Prefix = serviceName
}
if conf.UpdateInterval == "" {
conf.UpdateInterval = "1h" // Update once per hour
}
// Connectors
if conf.Connectors.GOCDB.Scope == "" {
conf.Connectors.GOCDB.Scope = "SM" // TODO(Daniel-WWU-IT): This might change in the future
}
// Exporters
addDefaultConnector := func(enabledList *[]string) {
if len(*enabledList) == 0 {
*enabledList = append(*enabledList, "*")
}
}
if conf.Exporters.WebAPI.Endpoint == "" {
conf.Exporters.WebAPI.Endpoint = "/sites"
}
addDefaultConnector(&conf.Exporters.WebAPI.EnabledConnectors)
if conf.Exporters.CS3API.Endpoint == "" {
conf.Exporters.CS3API.Endpoint = "/cs3"
}
addDefaultConnector(&conf.Exporters.CS3API.EnabledConnectors)
if len(conf.Exporters.CS3API.ElevatedServiceTypes) == 0 {
conf.Exporters.CS3API.ElevatedServiceTypes = append(conf.Exporters.CS3API.ElevatedServiceTypes, meshdata.EndpointGateway, meshdata.EndpointOCM, meshdata.EndpointWebdav)
}
if conf.Exporters.SiteLocations.Endpoint == "" {
conf.Exporters.SiteLocations.Endpoint = "/loc"
}
addDefaultConnector(&conf.Exporters.SiteLocations.EnabledConnectors)
addDefaultConnector(&conf.Exporters.PrometheusSD.EnabledConnectors)
addDefaultConnector(&conf.Exporters.Metrics.EnabledConnectors)
}
// New returns a new Mentix service.
func New(m map[string]interface{}, log *zerolog.Logger) (global.Service, error) {
// Prepare the configuration
conf, err := parseConfig(m)
if err != nil {
return nil, err
}
conf.Init()
// Create the Mentix instance
mntx, err := mentix.New(conf, log)
if err != nil {
return nil, errors.Wrap(err, "mentix: error creating Mentix")
}
// Create the service and start its background activity
s := &svc{
conf: conf,
mntx: mntx,
log: log,
stopSignal: make(chan struct{}),
}
s.startBackgroundService()
return s, nil
}
@@ -0,0 +1,94 @@
// Copyright 2018-2021 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 metrics
/*
This service initializes the metrics package according to the metrics configuration.
*/
import (
"net/http"
"os"
"github.com/mitchellh/mapstructure"
"github.com/opencloud-eu/reva/v2/pkg/logger"
"github.com/rs/zerolog"
"github.com/opencloud-eu/reva/v2/pkg/metrics"
"github.com/opencloud-eu/reva/v2/pkg/metrics/config"
"github.com/opencloud-eu/reva/v2/pkg/rhttp/global"
)
func init() {
global.Register(serviceName, New)
}
const (
serviceName = "metrics"
)
// Close is called when this service is being stopped.
func (s *svc) Close() error {
return nil
}
// Prefix returns the main endpoint of this service.
func (s *svc) Prefix() string {
// We use a dummy endpoint as the service is not expected to be exposed
// directly to the user, but just start a background process.
return "register_metrics"
}
// Unprotected returns all endpoints that can be queried without prior authorization.
func (s *svc) Unprotected() []string {
return []string{}
}
// Handler serves all HTTP requests.
func (s *svc) Handler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log := logger.New().With().Int("pid", os.Getpid()).Logger()
if _, err := w.Write([]byte("This is the metrics service.\n")); err != nil {
log.Error().Err(err).Msg("error writing metrics response")
}
})
}
// New returns a new metrics service.
func New(m map[string]interface{}, log *zerolog.Logger) (global.Service, error) {
// Prepare the configuration
conf := &config.Config{}
if err := mapstructure.Decode(m, conf); err != nil {
return nil, err
}
conf.Init()
// initialize metrics using the configuration
err := metrics.Init(conf)
if err != nil {
return nil, err
}
// Create the service
s := &svc{}
return s, nil
}
type svc struct {
}
@@ -0,0 +1,180 @@
// Copyright 2018-2023 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 ocmd
import (
"encoding/json"
"errors"
"fmt"
"mime"
"net/http"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
invitepb "github.com/cs3org/go-cs3apis/cs3/ocm/invite/v1beta1"
ocmprovider "github.com/cs3org/go-cs3apis/cs3/ocm/provider/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
"github.com/opencloud-eu/reva/v2/internal/http/services/reqres"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
ocmuser "github.com/opencloud-eu/reva/v2/pkg/ocm/user"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/opencloud-eu/reva/v2/pkg/utils"
)
type invitesHandler struct {
gatewaySelector *pool.Selector[gateway.GatewayAPIClient]
}
func (h *invitesHandler) init(c *config) error {
var err error
gatewaySelector, err := pool.GatewaySelector(c.GatewaySvc)
if err != nil {
return err
}
h.gatewaySelector = gatewaySelector
return nil
}
type acceptInviteRequest struct {
Token string `json:"token"`
UserID string `json:"userID"`
RecipientProvider string `json:"recipientProvider"`
Name string `json:"name"`
Email string `json:"email"`
}
// AcceptInvite informs avout an accepted invitation so that the users
// can initiate the OCM share creation.
func (h *invitesHandler) AcceptInvite(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
log := appctx.GetLogger(ctx)
req, err := getAcceptInviteRequest(r)
if err != nil {
reqres.WriteError(w, r, reqres.APIErrorInvalidParameter, "missing parameters in request", err)
return
}
if req.Token == "" || req.UserID == "" || req.RecipientProvider == "" {
reqres.WriteError(w, r, reqres.APIErrorInvalidParameter, "token, userID and recipiendProvider must not be null", nil)
return
}
clientIP, err := utils.GetClientIP(r)
if err != nil {
reqres.WriteError(w, r, reqres.APIErrorServerError, fmt.Sprintf("error retrieving client IP from request: %s", r.RemoteAddr), err)
return
}
providerInfo := ocmprovider.ProviderInfo{
Domain: req.RecipientProvider,
Services: []*ocmprovider.Service{
{
Host: clientIP,
},
},
}
gatewayClient, err := h.gatewaySelector.Next()
if err != nil {
reqres.WriteError(w, r, reqres.APIErrorServerError, "error getting gateway client", err)
return
}
providerAllowedResp, err := gatewayClient.IsProviderAllowed(ctx, &ocmprovider.IsProviderAllowedRequest{
Provider: &providerInfo,
})
if err != nil {
reqres.WriteError(w, r, reqres.APIErrorServerError, "error sending a grpc is provider allowed request", err)
return
}
if providerAllowedResp.Status.Code != rpc.Code_CODE_OK {
reqres.WriteError(w, r, reqres.APIErrorUntrustedService, "provider not trusted", errors.New(providerAllowedResp.Status.Message))
return
}
userObj := &userpb.User{
Id: &userpb.UserId{
OpaqueId: req.UserID,
Idp: req.RecipientProvider,
Type: userpb.UserType_USER_TYPE_FEDERATED,
},
Mail: req.Email,
DisplayName: req.Name,
}
acceptInviteRequest := &invitepb.AcceptInviteRequest{
InviteToken: &invitepb.InviteToken{
Token: req.Token,
},
RemoteUser: userObj,
}
acceptInviteResponse, err := gatewayClient.AcceptInvite(ctx, acceptInviteRequest)
if err != nil {
reqres.WriteError(w, r, reqres.APIErrorServerError, "error sending a grpc accept invite request", err)
return
}
if acceptInviteResponse.Status.Code != rpc.Code_CODE_OK {
switch acceptInviteResponse.Status.Code {
case rpc.Code_CODE_NOT_FOUND:
reqres.WriteError(w, r, reqres.APIErrorNotFound, "token not found", nil)
return
case rpc.Code_CODE_INVALID_ARGUMENT:
reqres.WriteError(w, r, reqres.APIErrorInvalidParameter, "token has expired", nil)
return
case rpc.Code_CODE_ALREADY_EXISTS:
reqres.WriteError(w, r, reqres.APIErrorAlreadyExist, "user already known", nil)
return
default:
reqres.WriteError(w, r, reqres.APIErrorServerError, "unexpected error: "+acceptInviteResponse.Status.Message, errors.New(acceptInviteResponse.Status.Message))
return
}
}
if err := json.NewEncoder(w).Encode(&user{
UserID: ocmuser.FederatedID(acceptInviteResponse.UserId, "").GetOpaqueId(),
Email: acceptInviteResponse.Email,
Name: acceptInviteResponse.DisplayName,
}); err != nil {
reqres.WriteError(w, r, reqres.APIErrorServerError, "error encoding response", err)
return
}
w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "application/json")
log.Info().Str("user", fmt.Sprintf("%s@%s", userObj.Id.OpaqueId, userObj.Id.Idp)).Str("token", req.Token).Msg("added to accepted users")
}
type user struct {
UserID string `json:"userID"`
Email string `json:"email"`
Name string `json:"name"`
}
func getAcceptInviteRequest(r *http.Request) (*acceptInviteRequest, error) {
var req acceptInviteRequest
contentType, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
if err == nil && contentType == "application/json" {
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
return nil, err
}
} else {
req.Token, req.UserID, req.RecipientProvider = r.FormValue("token"), r.FormValue("userID"), r.FormValue("recipientProvider")
req.Name, req.Email = r.FormValue("name"), r.FormValue("email")
}
return &req, nil
}
@@ -0,0 +1,66 @@
// Copyright 2018-2023 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 ocmd
import (
"io"
"mime"
"net/http"
"github.com/opencloud-eu/reva/v2/internal/http/services/reqres"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
)
// var validate = validator.New()
type notifHandler struct {
}
func (h *notifHandler) init(c *config) error {
return nil
}
// Notifications dispatches any notifications received from remote OCM sites
// according to the specifications at:
// https://cs3org.github.io/OCM-API/docs.html?branch=v1.1.0&repo=OCM-API&user=cs3org#/paths/~1notifications/post
func (h *notifHandler) Notifications(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
log := appctx.GetLogger(ctx)
req, err := getNotification(r)
if err != nil {
reqres.WriteError(w, r, reqres.APIErrorInvalidParameter, err.Error(), nil)
return
}
// TODO(lopresti) this is all to be implemented. For now we just log what we got
log.Debug().Msgf("Received OCM notification: %+v", req)
// this is to please Nextcloud
w.WriteHeader(http.StatusCreated)
}
func getNotification(r *http.Request) (string, error) {
// var req notificationRequest
contentType, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
if err == nil && contentType == "application/json" {
bytes, _ := io.ReadAll(r.Body)
return string(bytes), nil
}
return "", nil
}
@@ -0,0 +1,118 @@
// Copyright 2018-2023 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 ocmd
import (
"net/http"
"github.com/go-chi/chi/v5"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
"github.com/opencloud-eu/reva/v2/pkg/rhttp/global"
"github.com/opencloud-eu/reva/v2/pkg/sharedconf"
"github.com/opencloud-eu/reva/v2/pkg/utils/cfg"
"github.com/rs/zerolog"
)
func init() {
global.Register("ocmd", New)
}
type config struct {
Prefix string `mapstructure:"prefix"`
GatewaySvc string `mapstructure:"gatewaysvc" validate:"required"`
ExposeRecipientDisplayName bool `mapstructure:"expose_recipient_display_name"`
}
func (c *config) ApplyDefaults() {
c.GatewaySvc = sharedconf.GetGatewaySVC(c.GatewaySvc)
if c.Prefix == "" {
c.Prefix = "ocm"
}
}
type svc struct {
Conf *config
router chi.Router
}
// New returns a new ocmd object, that implements
// the OCM APIs specified in https://cs3org.github.io/OCM-API/docs.html
func New(m map[string]interface{}, log *zerolog.Logger) (global.Service, error) {
var c config
if err := cfg.Decode(m, &c); err != nil {
return nil, err
}
r := chi.NewRouter()
s := &svc{
Conf: &c,
router: r,
}
if err := s.routerInit(); err != nil {
return nil, err
}
return s, nil
}
func (s *svc) routerInit() error {
sharesHandler := new(sharesHandler)
invitesHandler := new(invitesHandler)
notifHandler := new(notifHandler)
if err := sharesHandler.init(s.Conf); err != nil {
return err
}
if err := invitesHandler.init(s.Conf); err != nil {
return err
}
if err := notifHandler.init(s.Conf); err != nil {
return err
}
s.router.Post("/shares", sharesHandler.CreateShare)
s.router.Post("/invite-accepted", invitesHandler.AcceptInvite)
s.router.Post("/notifications", notifHandler.Notifications)
return nil
}
// Close performs cleanup.
func (s *svc) Close() error {
return nil
}
func (s *svc) Prefix() string {
return s.Conf.Prefix
}
func (s *svc) Unprotected() []string {
return []string{"/invite-accepted", "/shares", "/notifications"}
}
func (s *svc) Handler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log := appctx.GetLogger(r.Context())
log.Debug().Str("path", r.URL.Path).Msg("ocm routing")
// unset raw path, otherwise chi uses it to route and then fails to match percent encoded path segments
r.URL.RawPath = ""
s.router.ServeHTTP(w, r)
})
}
@@ -0,0 +1,167 @@
// Copyright 2018-2023 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 ocmd
import (
"encoding/json"
"errors"
"fmt"
"reflect"
"strings"
ocm "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1"
providerv1beta1 "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
ocmshare "github.com/opencloud-eu/reva/v2/pkg/ocm/share"
utils "github.com/opencloud-eu/reva/v2/pkg/utils"
)
// Protocols is the list of protocols.
type Protocols []Protocol
// Protocol represents the way of access the resource
// in the OCM share.
type Protocol interface {
// ToOCMProtocol convert the protocol to a ocm Protocol struct
ToOCMProtocol() *ocm.Protocol
}
// protocols supported by the OCM API
// WebDAV contains the parameters for the WebDAV protocol.
type WebDAV struct {
SharedSecret string `json:"sharedSecret" validate:"required"`
Permissions []string `json:"permissions" validate:"required,dive,required,oneof=read write share"`
URL string `json:"url" validate:"required"`
}
// ToOCMProtocol convert the protocol to a ocm Protocol struct.
func (w *WebDAV) ToOCMProtocol() *ocm.Protocol {
perms := &ocm.SharePermissions{
Permissions: &providerv1beta1.ResourcePermissions{},
}
for _, p := range w.Permissions {
switch p {
case "read":
perms.Permissions.GetPath = true
perms.Permissions.GetQuota = true
perms.Permissions.InitiateFileDownload = true
perms.Permissions.ListContainer = true
perms.Permissions.ListRecycle = true
perms.Permissions.Stat = true
case "write":
perms.Permissions.InitiateFileUpload = true
perms.Permissions.RestoreRecycleItem = true
perms.Permissions.CreateContainer = true
perms.Permissions.Delete = true
perms.Permissions.Move = true
perms.Permissions.ListGrants = true
case "share":
perms.Reshare = true
}
}
return ocmshare.NewWebDAVProtocol(w.URL, w.SharedSecret, perms)
}
// Webapp contains the parameters for the Webapp protocol.
type Webapp struct {
URITemplate string `json:"uriTemplate" validate:"required"`
ViewMode string `json:"viewMode" validate:"required,dive,required,oneof=view read write"`
}
// ToOCMProtocol convert the protocol to a ocm Protocol struct.
func (w *Webapp) ToOCMProtocol() *ocm.Protocol {
return ocmshare.NewWebappProtocol(w.URITemplate, utils.GetAppViewMode(w.ViewMode))
}
// Datatx contains the parameters for the Datatx protocol.
type Datatx struct {
SharedSecret string `json:"sharedSecret" validate:"required"`
SourceURI string `json:"srcUri" validate:"required"`
Size uint64 `json:"size" validate:"required"`
}
// ToOCMProtocol convert the protocol to a ocm Protocol struct.
func (w *Datatx) ToOCMProtocol() *ocm.Protocol {
return ocmshare.NewTransferProtocol(w.SourceURI, w.SharedSecret, w.Size)
}
var protocolImpl = map[string]reflect.Type{
"webdav": reflect.TypeOf(WebDAV{}),
"webapp": reflect.TypeOf(Webapp{}),
"datatx": reflect.TypeOf(Datatx{}),
}
// UnmarshalJSON implements the Unmarshaler interface.
func (p *Protocols) UnmarshalJSON(data []byte) error {
var prot map[string]json.RawMessage
if err := json.Unmarshal(data, &prot); err != nil {
return err
}
*p = []Protocol{}
for name, d := range prot {
var res Protocol
// we do not support the OCM v1.0 properties for now, therefore just skip or bail out
if name == "name" {
continue
}
if name == "options" {
var opt map[string]any
if err := json.Unmarshal(d, &opt); err != nil || len(opt) > 0 {
return fmt.Errorf("protocol options not supported: %s", string(d))
}
continue
}
ctype, ok := protocolImpl[name]
if !ok {
return fmt.Errorf("protocol %s not recognised", name)
}
res = reflect.New(ctype).Interface().(Protocol)
if err := json.Unmarshal(d, &res); err != nil {
return err
}
*p = append(*p, res)
}
return nil
}
// MarshalJSON implements the Marshaler interface.
func (p Protocols) MarshalJSON() ([]byte, error) {
if len(p) == 0 {
return nil, errors.New("no protocol defined")
}
d := make(map[string]any)
for _, prot := range p {
d[getProtocolName(prot)] = prot
}
// fill in the OCM v1.0 properties
d["name"] = "multi"
d["options"] = map[string]any{}
return json.Marshal(d)
}
func getProtocolName(p Protocol) string {
n := reflect.TypeOf(p).String()
s := strings.Split(n, ".")
return strings.ToLower(s[len(s)-1])
}
@@ -0,0 +1,262 @@
// Copyright 2018-2023 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 ocmd
import (
"encoding/json"
"errors"
"fmt"
"mime"
"net/http"
"strings"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
providerpb "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
ocmcore "github.com/cs3org/go-cs3apis/cs3/ocm/core/v1beta1"
ocmprovider "github.com/cs3org/go-cs3apis/cs3/ocm/provider/v1beta1"
ocm "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
"github.com/go-playground/validator/v10"
"github.com/opencloud-eu/reva/v2/internal/http/services/reqres"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/opencloud-eu/reva/v2/pkg/utils"
)
var validate = validator.New()
type sharesHandler struct {
gatewaySelector *pool.Selector[gateway.GatewayAPIClient]
exposeRecipientDisplayName bool
}
func (h *sharesHandler) init(c *config) error {
var err error
gatewaySelector, err := pool.GatewaySelector(c.GatewaySvc)
if err != nil {
return err
}
h.gatewaySelector = gatewaySelector
h.exposeRecipientDisplayName = c.ExposeRecipientDisplayName
return nil
}
type createShareRequest struct {
ShareWith string `json:"shareWith" validate:"required"` // identifier of the recipient of the share
Name string `json:"name" validate:"required"` // name of the resource
Description string `json:"description"` // (optional) description of the resource
ProviderID string `json:"providerId" validate:"required"` // unique identifier of the resource at provider side
Owner string `json:"owner" validate:"required"` // unique identifier of the owner at provider side
Sender string `json:"sender" validate:"required"` // unique indentifier of the user who wants to share the resource at provider side
OwnerDisplayName string `json:"ownerDisplayName"` // display name of the owner of the resource
SenderDisplayName string `json:"senderDisplayName"` // dispay name of the user who wants to share the resource
ShareType string `json:"shareType" validate:"required,oneof=user group"` // recipient share type (user or group)
ResourceType string `json:"resourceType" validate:"required,oneof=file folder"`
Expiration uint64 `json:"expiration"`
Protocols Protocols `json:"protocol" validate:"required"`
}
// CreateShare sends all the informations to the consumer needed to start
// synchronization between the two services.
func (h *sharesHandler) CreateShare(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
log := appctx.GetLogger(ctx)
req, err := getCreateShareRequest(r)
if err != nil {
reqres.WriteError(w, r, reqres.APIErrorInvalidParameter, err.Error(), nil)
return
}
_, meshProvider, err := getIDAndMeshProvider(req.Sender)
log.Debug().Msgf("Determined Mesh Provider '%s' from req.Sender '%s'", meshProvider, req.Sender)
if err != nil {
reqres.WriteError(w, r, reqres.APIErrorInvalidParameter, err.Error(), nil)
return
}
clientIP, err := utils.GetClientIP(r)
if err != nil {
reqres.WriteError(w, r, reqres.APIErrorServerError, fmt.Sprintf("error retrieving client IP from request: %s", r.RemoteAddr), err)
return
}
providerInfo := ocmprovider.ProviderInfo{
Domain: meshProvider,
Services: []*ocmprovider.Service{
{
Host: clientIP,
},
},
}
gatewayClient, err := h.gatewaySelector.Next()
if err != nil {
reqres.WriteError(w, r, reqres.APIErrorServerError, "error getting gateway client", err)
return
}
providerAllowedResp, err := gatewayClient.IsProviderAllowed(ctx, &ocmprovider.IsProviderAllowedRequest{
Provider: &providerInfo,
})
if err != nil {
reqres.WriteError(w, r, reqres.APIErrorServerError, "error sending a grpc is provider allowed request", err)
return
}
if providerAllowedResp.Status.Code != rpc.Code_CODE_OK {
reqres.WriteError(w, r, reqres.APIErrorUnauthenticated, "provider not authorized", errors.New(providerAllowedResp.Status.Message))
return
}
shareWith, _, err := getIDAndMeshProvider(req.ShareWith)
if err != nil {
reqres.WriteError(w, r, reqres.APIErrorInvalidParameter, err.Error(), nil)
return
}
userRes, err := gatewayClient.GetUser(ctx, &userpb.GetUserRequest{
UserId: &userpb.UserId{OpaqueId: shareWith}, SkipFetchingUserGroups: true,
})
if err != nil {
reqres.WriteError(w, r, reqres.APIErrorServerError, "error searching recipient", err)
return
}
if userRes.Status.Code != rpc.Code_CODE_OK {
reqres.WriteError(w, r, reqres.APIErrorNotFound, "user not found", errors.New(userRes.Status.Message))
return
}
owner, err := getUserIDFromOCMUser(req.Owner)
if err != nil {
reqres.WriteError(w, r, reqres.APIErrorInvalidParameter, err.Error(), nil)
return
}
sender, err := getUserIDFromOCMUser(req.Sender)
if err != nil {
reqres.WriteError(w, r, reqres.APIErrorInvalidParameter, err.Error(), nil)
return
}
createShareReq := &ocmcore.CreateOCMCoreShareRequest{
Description: req.Description,
Name: req.Name,
ResourceId: req.ProviderID,
Owner: owner,
Sender: sender,
ShareWith: userRes.User.Id,
ResourceType: getResourceTypeFromOCMRequest(req.ResourceType),
ShareType: getOCMShareType(req.ShareType),
Protocols: getProtocols(req.Protocols),
}
if req.Expiration != 0 {
createShareReq.Expiration = &types.Timestamp{
Seconds: req.Expiration,
}
}
createShareResp, err := gatewayClient.CreateOCMCoreShare(ctx, createShareReq)
if err != nil {
reqres.WriteError(w, r, reqres.APIErrorServerError, "error creating ocm share", err)
return
}
if userRes.Status.Code != rpc.Code_CODE_OK {
// TODO: define errors in the cs3apis
reqres.WriteError(w, r, reqres.APIErrorServerError, "error creating ocm share", errors.New(createShareResp.Status.Message))
return
}
response := map[string]any{}
if h.exposeRecipientDisplayName {
response["recipientDisplayName"] = userRes.User.DisplayName
}
_ = json.NewEncoder(w).Encode(response)
w.WriteHeader(http.StatusCreated)
}
func getUserIDFromOCMUser(user string) (*userpb.UserId, error) {
id, idp, err := getIDAndMeshProvider(user)
if err != nil {
return nil, err
}
return &userpb.UserId{
OpaqueId: id,
Idp: idp,
// the remote user is a federated account for the local reva
Type: userpb.UserType_USER_TYPE_FEDERATED,
}, nil
}
func getIDAndMeshProvider(user string) (string, string, error) {
// the user is in the form of dimitri@apiwise.nl
split := strings.Split(user, "@")
if len(split) < 2 {
return "", "", errors.New("not in the form <id>@<provider>")
}
return strings.Join(split[:len(split)-1], "@"), split[len(split)-1], nil
}
func getCreateShareRequest(r *http.Request) (*createShareRequest, error) {
var req createShareRequest
contentType, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
if err == nil && contentType == "application/json" {
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
return nil, err
}
} else {
return nil, errors.New("body request not recognised")
}
// validate the request
if err := validate.Struct(req); err != nil {
return nil, err
}
return &req, nil
}
func getResourceTypeFromOCMRequest(t string) providerpb.ResourceType {
switch t {
case "file":
return providerpb.ResourceType_RESOURCE_TYPE_FILE
case "folder":
return providerpb.ResourceType_RESOURCE_TYPE_CONTAINER
default:
return providerpb.ResourceType_RESOURCE_TYPE_INVALID
}
}
func getOCMShareType(t string) ocm.ShareType {
if t == "user" {
return ocm.ShareType_SHARE_TYPE_USER
}
return ocm.ShareType_SHARE_TYPE_GROUP
}
func getProtocols(p Protocols) []*ocm.Protocol {
prot := make([]*ocm.Protocol, 0, len(p))
for _, data := range p {
prot = append(prot, data.ToOCMProtocol())
}
return prot
}
@@ -0,0 +1,71 @@
// Copyright 2018-2021 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 (
"encoding/hex"
"net/http"
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/config"
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/net"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
"github.com/opencloud-eu/reva/v2/pkg/rhttp/router"
)
// AvatarsHandler handles avatar requests
type AvatarsHandler struct {
}
func (h *AvatarsHandler) init(c *config.Config) error {
return nil
}
// Handler handles requests
func (h *AvatarsHandler) Handler(s *svc) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
log := appctx.GetLogger(ctx)
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
s.handleOptions(w, r)
return
}
_, r.URL.Path = router.ShiftPath(r.URL.Path)
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)
if err != nil {
log.Error().Err(err).Msg("error decoding string")
w.WriteHeader(http.StatusInternalServerError)
}
w.Header().Set(net.HeaderContentType, "image/png")
if _, err := w.Write(decoded); err != nil {
log.Error().Err(err).Msg("error writing data response")
}
return
}
w.WriteHeader(http.StatusNotFound)
})
}
@@ -0,0 +1,89 @@
package config
import "github.com/opencloud-eu/reva/v2/pkg/sharedconf"
// Config holds the config options that need to be passed down to all ocdav handlers
type Config struct {
Prefix string `mapstructure:"prefix"`
// FilesNamespace prefixes the namespace, optionally with user information.
// Example: if FilesNamespace is /users/{{substr 0 1 .Username}}/{{.Username}}
// and received path is /docs the internal path will be:
// /users/<first char of username>/<username>/docs
FilesNamespace string `mapstructure:"files_namespace"`
// WebdavNamespace prefixes the namespace, optionally with user information.
// Example: if WebdavNamespace is /users/{{substr 0 1 .Username}}/{{.Username}}
// and received path is /docs the internal path will be:
// /users/<first char of username>/<username>/docs
WebdavNamespace string `mapstructure:"webdav_namespace"`
SharesNamespace string `mapstructure:"shares_namespace"`
OCMNamespace string `mapstructure:"ocm_namespace"`
GatewaySvc string `mapstructure:"gatewaysvc"`
Timeout int64 `mapstructure:"timeout"`
Insecure bool `mapstructure:"insecure"`
// If true, HTTP COPY will expect the HTTP-TPC (third-party copy) headers
EnableHTTPTpc bool `mapstructure:"enable_http_tpc"`
PublicURL string `mapstructure:"public_url"`
FavoriteStorageDriver string `mapstructure:"favorite_storage_driver"`
FavoriteStorageDrivers map[string]map[string]interface{} `mapstructure:"favorite_storage_drivers"`
Version string `mapstructure:"version"`
VersionString string `mapstructure:"version_string"`
Edition string `mapstructure:"edition"`
Product string `mapstructure:"product"`
ProductName string `mapstructure:"product_name"`
ProductVersion string `mapstructure:"product_version"`
AllowPropfindDepthInfinitiy bool `mapstructure:"allow_depth_infinity"`
TransferSharedSecret string `mapstructure:"transfer_shared_secret"`
NameValidation NameValidation `mapstructure:"validation"`
MachineAuthAPIKey string `mapstructure:"machine_auth_apikey"`
}
// NameValidation is the validation configuration for file and folder names
type NameValidation struct {
InvalidChars []string `mapstructure:"invalid_chars"`
MaxLength int `mapstructure:"max_length"`
}
// Init initializes the configuration
func (c *Config) Init() {
// note: default c.Prefix is an empty string
c.GatewaySvc = sharedconf.GetGatewaySVC(c.GatewaySvc)
if c.FavoriteStorageDriver == "" {
c.FavoriteStorageDriver = "memory"
}
if c.Version == "" {
c.Version = "10.0.11.5"
}
if c.VersionString == "" {
c.VersionString = "10.0.11"
}
if c.Product == "" {
c.Product = "reva"
}
if c.ProductName == "" {
c.ProductName = "reva"
}
if c.ProductVersion == "" {
c.ProductVersion = "10.0.11"
}
if c.Edition == "" {
c.Edition = "community"
}
if c.NameValidation.InvalidChars == nil {
c.NameValidation.InvalidChars = []string{"\f", "\r", "\n", "\\"}
}
if c.NameValidation.MaxLength == 0 {
c.NameValidation.MaxLength = 255
}
}
@@ -0,0 +1,20 @@
package ocdav
import (
"context"
cs3storage "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
)
type tokenStatInfoKey struct{}
// ContextWithTokenStatInfo adds the token stat info to the context
func ContextWithTokenStatInfo(ctx context.Context, info *cs3storage.ResourceInfo) context.Context {
return context.WithValue(ctx, tokenStatInfoKey{}, info)
}
// TokenStatInfoFromContext returns the token stat info from the context
func TokenStatInfoFromContext(ctx context.Context) (*cs3storage.ResourceInfo, bool) {
v, ok := ctx.Value(tokenStatInfoKey{}).(*cs3storage.ResourceInfo)
return v, ok
}
@@ -0,0 +1,756 @@
// Copyright 2018-2021 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 (
"context"
"fmt"
"net/http"
"path"
"path/filepath"
"strconv"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
typespb "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/opencloud-eu/reva/v2/internal/http/services/datagateway"
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/errors"
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/net"
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/spacelookup"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/opencloud-eu/reva/v2/pkg/rhttp"
"github.com/opencloud-eu/reva/v2/pkg/rhttp/router"
"github.com/opencloud-eu/reva/v2/pkg/storagespace"
"github.com/opencloud-eu/reva/v2/pkg/utils"
"github.com/rs/zerolog"
)
type copy struct {
source *provider.Reference
sourceInfo *provider.ResourceInfo
destination *provider.Reference
depth net.Depth
successCode int
}
func (s *svc) handlePathCopy(w http.ResponseWriter, r *http.Request, ns string) {
ctx, span := appctx.GetTracerProvider(r.Context()).Tracer(tracerName).Start(r.Context(), "copy")
defer span.End()
if r.Body != http.NoBody {
w.WriteHeader(http.StatusUnsupportedMediaType)
b, err := errors.Marshal(http.StatusUnsupportedMediaType, "body must be empty", "", "")
errors.HandleWebdavError(appctx.GetLogger(ctx), w, b, err)
return
}
if s.c.EnableHTTPTpc {
if r.Header.Get("Source") != "" {
// HTTP Third-Party Copy Pull mode
s.handleTPCPull(ctx, w, r, ns)
return
} else if r.Header.Get("Destination") != "" {
// HTTP Third-Party Copy Push mode
s.handleTPCPush(ctx, w, r, ns)
return
}
}
// Local copy: in this case Destination is mandatory
src := path.Join(ns, r.URL.Path)
dh := r.Header.Get(net.HeaderDestination)
baseURI := r.Context().Value(net.CtxKeyBaseURI).(string)
dst, err := net.ParseDestination(baseURI, dh)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
b, err := errors.Marshal(http.StatusBadRequest, "failed to extract destination", "", "")
errors.HandleWebdavError(appctx.GetLogger(ctx), w, b, err)
return
}
if err := ValidateName(filename(src), s.nameValidators); err != nil {
w.WriteHeader(http.StatusBadRequest)
b, err := errors.Marshal(http.StatusBadRequest, "source failed naming rules", "", "")
errors.HandleWebdavError(appctx.GetLogger(ctx), w, b, err)
return
}
if err := ValidateDestination(filename(dst), s.nameValidators); err != nil {
w.WriteHeader(http.StatusBadRequest)
b, err := errors.Marshal(http.StatusBadRequest, "destination failed naming rules", "", "")
errors.HandleWebdavError(appctx.GetLogger(ctx), w, b, err)
return
}
dst = path.Join(ns, dst)
sublog := appctx.GetLogger(ctx).With().Str("src", src).Str("dst", dst).Logger()
srcSpace, status, err := spacelookup.LookUpStorageSpaceForPath(ctx, s.gatewaySelector, src)
if err != nil {
sublog.Error().Err(err).Str("path", src).Msg("failed to look up storage space")
w.WriteHeader(http.StatusInternalServerError)
return
}
if status.Code != rpc.Code_CODE_OK {
errors.HandleErrorStatus(&sublog, w, status)
return
}
dstSpace, status, err := spacelookup.LookUpStorageSpaceForPath(ctx, s.gatewaySelector, dst)
if err != nil {
sublog.Error().Err(err).Str("path", dst).Msg("failed to look up storage space")
w.WriteHeader(http.StatusInternalServerError)
return
}
if status.Code != rpc.Code_CODE_OK {
errors.HandleErrorStatus(&sublog, w, status)
return
}
cp := s.prepareCopy(ctx, w, r, spacelookup.MakeRelativeReference(srcSpace, src, false), spacelookup.MakeRelativeReference(dstSpace, dst, false), &sublog, dstSpace.GetRoot().GetStorageId() == utils.ShareStorageProviderID)
if cp == nil {
return
}
if err := s.executePathCopy(ctx, s.gatewaySelector, w, r, cp); err != nil {
sublog.Error().Err(err).Str("depth", cp.depth.String()).Msg("error executing path copy")
w.WriteHeader(http.StatusInternalServerError)
}
w.WriteHeader(cp.successCode)
}
func (s *svc) executePathCopy(ctx context.Context, selector pool.Selectable[gateway.GatewayAPIClient], w http.ResponseWriter, r *http.Request, cp *copy) error {
log := appctx.GetLogger(ctx)
log.Debug().Str("src", cp.sourceInfo.Path).Str("dst", cp.destination.Path).Msg("descending")
client, err := selector.Next()
if err != nil {
return err
}
var fileid string
if cp.sourceInfo.Type == provider.ResourceType_RESOURCE_TYPE_CONTAINER {
// create dir
createReq := &provider.CreateContainerRequest{
Ref: cp.destination,
}
createRes, err := client.CreateContainer(ctx, createReq)
if err != nil {
log.Error().Err(err).Msg("error performing create container grpc request")
return err
}
if createRes.Status.Code != rpc.Code_CODE_OK {
if createRes.Status.Code == rpc.Code_CODE_PERMISSION_DENIED {
w.WriteHeader(http.StatusForbidden)
m := fmt.Sprintf("Permission denied to create %v", createReq.Ref.Path)
b, err := errors.Marshal(http.StatusForbidden, m, "", "")
errors.HandleWebdavError(log, w, b, err)
}
return nil
}
// TODO: also copy properties: https://tools.ietf.org/html/rfc4918#section-9.8.2
if cp.depth != net.DepthInfinity {
return nil
}
// descend for children
listReq := &provider.ListContainerRequest{
Ref: cp.source,
}
res, err := client.ListContainer(ctx, listReq)
if err != nil {
return err
}
if res.Status.Code != rpc.Code_CODE_OK {
w.WriteHeader(http.StatusInternalServerError)
return nil
}
for i := range res.Infos {
child := filepath.Base(res.Infos[i].Path)
src := &provider.Reference{
ResourceId: cp.source.ResourceId,
Path: utils.MakeRelativePath(filepath.Join(cp.source.Path, child)),
}
childDst := &provider.Reference{
ResourceId: cp.destination.ResourceId,
Path: utils.MakeRelativePath(filepath.Join(cp.destination.Path, child)),
}
err := s.executePathCopy(ctx, selector, w, r, &copy{source: src, sourceInfo: res.Infos[i], destination: childDst, depth: cp.depth, successCode: cp.successCode})
if err != nil {
return err
}
}
// we need to stat again to get the fileid
r, err := client.Stat(ctx, &provider.StatRequest{Ref: cp.destination})
if err != nil {
return err
}
if r.GetStatus().GetCode() != rpc.Code_CODE_OK {
return fmt.Errorf("status code %d", r.GetStatus().GetCode())
}
fileid = storagespace.FormatResourceID(r.GetInfo().GetId())
} else {
// copy file
// 1. get download url
dReq := &provider.InitiateFileDownloadRequest{
Ref: cp.source,
}
dRes, err := client.InitiateFileDownload(ctx, dReq)
if err != nil {
return err
}
if dRes.Status.Code != rpc.Code_CODE_OK {
return fmt.Errorf("status code %d", dRes.Status.Code)
}
var downloadEP, downloadToken string
for _, p := range dRes.Protocols {
if p.Protocol == "spaces" {
downloadEP, downloadToken = p.DownloadEndpoint, p.Token
}
}
// 2. get upload url
uReq := &provider.InitiateFileUploadRequest{
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(strconv.FormatUint(cp.sourceInfo.GetSize(), 10)),
},
},
},
}
uRes, err := client.InitiateFileUpload(ctx, uReq)
if err != nil {
return err
}
if uRes.Status.Code != rpc.Code_CODE_OK {
if uRes.Status.Code == rpc.Code_CODE_PERMISSION_DENIED {
w.WriteHeader(http.StatusForbidden)
m := fmt.Sprintf("Permissions denied to create %v", uReq.Ref.Path)
b, err := errors.Marshal(http.StatusForbidden, m, "", "")
errors.HandleWebdavError(log, w, b, err)
return nil
}
errors.HandleErrorStatus(log, w, uRes.Status)
return nil
}
var uploadEP, uploadToken string
for _, p := range uRes.Protocols {
if p.Protocol == "simple" {
uploadEP, uploadToken = p.UploadEndpoint, p.Token
}
}
// 3. do download
httpDownloadReq, err := rhttp.NewRequest(ctx, "GET", downloadEP, nil)
if err != nil {
return err
}
httpDownloadReq.Header.Set(datagateway.TokenTransportHeader, downloadToken)
httpDownloadRes, err := s.client.Do(httpDownloadReq)
if err != nil {
return err
}
defer httpDownloadRes.Body.Close()
if httpDownloadRes.StatusCode == http.StatusForbidden {
w.WriteHeader(http.StatusForbidden)
b, err := errors.Marshal(http.StatusForbidden, http.StatusText(http.StatusForbidden), "", strconv.Itoa(http.StatusForbidden))
errors.HandleWebdavError(log, w, b, err)
return nil
}
if httpDownloadRes.StatusCode != http.StatusOK {
return fmt.Errorf("status code %d", httpDownloadRes.StatusCode)
}
// 4. do upload
httpUploadReq, err := rhttp.NewRequest(ctx, "PUT", uploadEP, httpDownloadRes.Body)
if err != nil {
return err
}
httpUploadReq.Header.Set(datagateway.TokenTransportHeader, uploadToken)
httpUploadReq.ContentLength = int64(cp.sourceInfo.GetSize())
httpUploadRes, err := s.client.Do(httpUploadReq)
if err != nil {
return err
}
defer httpUploadRes.Body.Close()
if httpUploadRes.StatusCode != http.StatusOK {
return err
}
fileid = httpUploadRes.Header.Get(net.HeaderOCFileID)
}
w.Header().Set(net.HeaderOCFileID, fileid)
return nil
}
func (s *svc) handleSpacesCopy(w http.ResponseWriter, r *http.Request, spaceID string) {
ctx, span := appctx.GetTracerProvider(r.Context()).Tracer(tracerName).Start(r.Context(), "spaces_copy")
defer span.End()
if r.Body != http.NoBody {
w.WriteHeader(http.StatusUnsupportedMediaType)
b, err := errors.Marshal(http.StatusUnsupportedMediaType, "body must be empty", "", "")
errors.HandleWebdavError(appctx.GetLogger(ctx), w, b, err)
return
}
dh := r.Header.Get(net.HeaderDestination)
baseURI := r.Context().Value(net.CtxKeyBaseURI).(string)
dst, err := net.ParseDestination(baseURI, dh)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
sublog := appctx.GetLogger(ctx).With().Str("spaceid", spaceID).Str("path", r.URL.Path).Str("destination", dst).Logger()
srcRef, err := spacelookup.MakeStorageSpaceReference(spaceID, r.URL.Path)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
dstSpaceID, dstRelPath := router.ShiftPath(dst)
dstRef, err := spacelookup.MakeStorageSpaceReference(dstSpaceID, dstRelPath)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
cp := s.prepareCopy(ctx, w, r, &srcRef, &dstRef, &sublog, dstRef.GetResourceId().GetStorageId() == utils.ShareStorageProviderID)
if cp == nil {
return
}
err = s.executeSpacesCopy(ctx, w, s.gatewaySelector, cp)
if err != nil {
sublog.Error().Err(err).Str("depth", cp.depth.String()).Msg("error descending directory")
w.WriteHeader(http.StatusInternalServerError)
}
w.WriteHeader(cp.successCode)
}
func (s *svc) executeSpacesCopy(ctx context.Context, w http.ResponseWriter, selector pool.Selectable[gateway.GatewayAPIClient], cp *copy) error {
log := appctx.GetLogger(ctx)
log.Debug().Interface("src", cp.sourceInfo).Interface("dst", cp.destination).Msg("descending")
client, err := selector.Next()
if err != nil {
return err
}
var fileid string
if cp.sourceInfo.Type == provider.ResourceType_RESOURCE_TYPE_CONTAINER {
// create dir
createReq := &provider.CreateContainerRequest{
Ref: cp.destination,
}
createRes, err := client.CreateContainer(ctx, createReq)
if err != nil {
log.Error().Err(err).Msg("error performing create container grpc request")
return err
}
if createRes.Status.Code != rpc.Code_CODE_OK {
if createRes.Status.Code == rpc.Code_CODE_PERMISSION_DENIED {
w.WriteHeader(http.StatusForbidden)
// TODO path could be empty or relative...
m := fmt.Sprintf("Permission denied to create %v", createReq.Ref.Path)
b, err := errors.Marshal(http.StatusForbidden, m, "", "")
errors.HandleWebdavError(log, w, b, err)
}
return nil
}
// TODO: also copy properties: https://tools.ietf.org/html/rfc4918#section-9.8.2
if cp.depth != net.DepthInfinity {
return nil
}
// descend for children
listReq := &provider.ListContainerRequest{Ref: &provider.Reference{ResourceId: cp.sourceInfo.Id, Path: "."}}
res, err := client.ListContainer(ctx, listReq)
if err != nil {
return err
}
if res.Status.Code != rpc.Code_CODE_OK {
w.WriteHeader(http.StatusInternalServerError)
return nil
}
for i := range res.Infos {
childRef := &provider.Reference{
ResourceId: cp.destination.ResourceId,
Path: utils.MakeRelativePath(path.Join(cp.destination.Path, res.Infos[i].Path)),
}
err := s.executeSpacesCopy(ctx, w, selector, &copy{sourceInfo: res.Infos[i], destination: childRef, depth: cp.depth, successCode: cp.successCode})
if err != nil {
return err
}
}
// we need to stat again to get the fileid
r, err := client.Stat(ctx, &provider.StatRequest{Ref: cp.destination})
if err != nil {
return err
}
if r.GetStatus().GetCode() != rpc.Code_CODE_OK {
return fmt.Errorf("stat: status code %d", r.GetStatus().GetCode())
}
fileid = storagespace.FormatResourceID(r.GetInfo().GetId())
} else {
// copy file
// 1. get download url
dReq := &provider.InitiateFileDownloadRequest{Ref: &provider.Reference{ResourceId: cp.sourceInfo.Id, Path: "."}}
dRes, err := client.InitiateFileDownload(ctx, dReq)
if err != nil {
return err
}
if dRes.Status.Code != rpc.Code_CODE_OK {
return fmt.Errorf("status code %d", dRes.Status.Code)
}
var downloadEP, downloadToken string
for _, p := range dRes.Protocols {
if p.Protocol == "spaces" {
downloadEP, downloadToken = p.DownloadEndpoint, p.Token
}
}
// 2. get upload url
uReq := &provider.InitiateFileUploadRequest{
Ref: cp.destination,
Opaque: &typespb.Opaque{
Map: map[string]*typespb.OpaqueEntry{
net.HeaderUploadLength: {
Decoder: "plain",
// TODO: handle case where size is not known in advance
Value: []byte(strconv.FormatUint(cp.sourceInfo.GetSize(), 10)),
},
},
},
}
uRes, err := client.InitiateFileUpload(ctx, uReq)
if err != nil {
return err
}
if uRes.Status.Code != rpc.Code_CODE_OK {
if uRes.Status.Code == rpc.Code_CODE_PERMISSION_DENIED {
w.WriteHeader(http.StatusForbidden)
// TODO path can be empty or relative
m := fmt.Sprintf("Permissions denied to create %v", uReq.Ref.Path)
b, err := errors.Marshal(http.StatusForbidden, m, "", "")
errors.HandleWebdavError(log, w, b, err)
return nil
}
errors.HandleErrorStatus(log, w, uRes.Status)
return nil
}
var uploadEP, uploadToken string
for _, p := range uRes.Protocols {
if p.Protocol == "simple" {
uploadEP, uploadToken = p.UploadEndpoint, p.Token
}
}
// 3. do download
httpDownloadReq, err := rhttp.NewRequest(ctx, http.MethodGet, downloadEP, nil)
if err != nil {
return err
}
if downloadToken != "" {
httpDownloadReq.Header.Set(datagateway.TokenTransportHeader, downloadToken)
}
httpDownloadRes, err := s.client.Do(httpDownloadReq)
if err != nil {
return err
}
defer httpDownloadRes.Body.Close()
if httpDownloadRes.StatusCode == http.StatusForbidden {
w.WriteHeader(http.StatusForbidden)
b, err := errors.Marshal(http.StatusForbidden, http.StatusText(http.StatusForbidden), "", strconv.Itoa(http.StatusForbidden))
errors.HandleWebdavError(log, w, b, err)
return nil
}
if httpDownloadRes.StatusCode != http.StatusOK {
return fmt.Errorf("status code %d", httpDownloadRes.StatusCode)
}
// 4. do upload
httpUploadReq, err := rhttp.NewRequest(ctx, http.MethodPut, uploadEP, httpDownloadRes.Body)
if err != nil {
return err
}
httpUploadReq.Header.Set(datagateway.TokenTransportHeader, uploadToken)
httpUploadReq.ContentLength = int64(cp.sourceInfo.GetSize())
httpUploadRes, err := s.client.Do(httpUploadReq)
if err != nil {
return err
}
defer httpUploadRes.Body.Close()
if httpUploadRes.StatusCode != http.StatusOK {
return err
}
fileid = httpUploadRes.Header.Get(net.HeaderOCFileID)
}
w.Header().Set(net.HeaderOCFileID, fileid)
return nil
}
func (s *svc) prepareCopy(ctx context.Context, w http.ResponseWriter, r *http.Request, srcRef, dstRef *provider.Reference, log *zerolog.Logger, destInShareJail bool) *copy {
isChild, err := s.referenceIsChildOf(ctx, s.gatewaySelector, dstRef, srcRef)
if err != nil {
switch err.(type) {
case errtypes.IsNotSupported:
log.Error().Err(err).Msg("can not detect recursive copy operation. missing machine auth configuration?")
w.WriteHeader(http.StatusForbidden)
default:
log.Error().Err(err).Msg("error while trying to detect recursive copy operation")
w.WriteHeader(http.StatusInternalServerError)
}
}
if isChild {
w.WriteHeader(http.StatusConflict)
b, err := errors.Marshal(http.StatusBadRequest, "can not copy a folder into one of its children", "", "")
errors.HandleWebdavError(log, w, b, err)
return nil
}
isParent, err := s.referenceIsChildOf(ctx, s.gatewaySelector, srcRef, dstRef)
if err != nil {
switch err.(type) {
case errtypes.IsNotFound:
isParent = false
case errtypes.IsNotSupported:
log.Error().Err(err).Msg("can not detect recursive copy operation. missing machine auth configuration?")
w.WriteHeader(http.StatusForbidden)
return nil
default:
log.Error().Err(err).Msg("error while trying to detect recursive copy operation")
w.WriteHeader(http.StatusInternalServerError)
return nil
}
}
if isParent {
w.WriteHeader(http.StatusConflict)
b, err := errors.Marshal(http.StatusBadRequest, "can not copy a folder into its parent", "", "")
errors.HandleWebdavError(log, w, b, err)
return nil
}
if srcRef.Path == dstRef.Path && srcRef.ResourceId == dstRef.ResourceId {
w.WriteHeader(http.StatusConflict)
b, err := errors.Marshal(http.StatusBadRequest, "source and destination are the same", "", "")
errors.HandleWebdavError(log, w, b, err)
return nil
}
oh := r.Header.Get(net.HeaderOverwrite)
overwrite, err := net.ParseOverwrite(oh)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
m := fmt.Sprintf("Overwrite header is set to incorrect value %v", overwrite)
b, err := errors.Marshal(http.StatusBadRequest, m, "", "")
errors.HandleWebdavError(log, w, b, err)
return nil
}
dh := r.Header.Get(net.HeaderDepth)
depth, err := net.ParseDepth(dh)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
m := fmt.Sprintf("Depth header is set to incorrect value %v", dh)
b, err := errors.Marshal(http.StatusBadRequest, m, "", "")
errors.HandleWebdavError(log, w, b, err)
return nil
}
if dh == "" {
// net.ParseDepth returns "1" for an empty value but copy expects "infinity"
// so we overwrite it here
depth = net.DepthInfinity
}
log.Debug().Bool("overwrite", overwrite).Str("depth", depth.String()).Msg("copy")
client, err := s.gatewaySelector.Next()
if err != nil {
log.Error().Err(err).Msg("error selecting next client")
w.WriteHeader(http.StatusInternalServerError)
return nil
}
srcStatReq := &provider.StatRequest{Ref: srcRef}
srcStatRes, err := client.Stat(ctx, srcStatReq)
switch {
case err != nil:
log.Error().Err(err).Msg("error sending grpc stat request")
w.WriteHeader(http.StatusInternalServerError)
return nil
case srcStatRes.Status.Code == rpc.Code_CODE_NOT_FOUND:
errors.HandleErrorStatus(log, w, srcStatRes.Status)
m := fmt.Sprintf("Resource %v not found", srcStatReq.Ref.Path)
b, err := errors.Marshal(http.StatusNotFound, m, "", "")
errors.HandleWebdavError(log, w, b, err)
return nil
case srcStatRes.Status.Code != rpc.Code_CODE_OK:
errors.HandleErrorStatus(log, w, srcStatRes.Status)
return nil
}
if utils.IsSpaceRoot(srcStatRes.GetInfo()) {
log.Error().Msg("the source is disallowed")
w.WriteHeader(http.StatusBadRequest)
return nil
}
dstStatReq := &provider.StatRequest{Ref: dstRef}
dstStatRes, err := client.Stat(ctx, dstStatReq)
switch {
case err != nil:
log.Error().Err(err).Msg("error sending grpc stat request")
w.WriteHeader(http.StatusInternalServerError)
return nil
case dstStatRes.Status.Code != rpc.Code_CODE_OK && dstStatRes.Status.Code != rpc.Code_CODE_NOT_FOUND:
errors.HandleErrorStatus(log, w, dstStatRes.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 {
log.Warn().Bool("overwrite", overwrite).Msg("dst already exists")
w.WriteHeader(http.StatusPreconditionFailed)
m := fmt.Sprintf("Could not overwrite Resource %v", dstRef.Path)
b, err := errors.Marshal(http.StatusPreconditionFailed, m, "", "")
errors.HandleWebdavError(log, w, b, err) // 412, see https://tools.ietf.org/html/rfc4918#section-9.8.5
return nil
}
if utils.IsSpaceRoot(dstStatRes.GetInfo()) {
log.Error().Msg("overwriting is not allowed")
w.WriteHeader(http.StatusBadRequest)
return nil
}
// delete existing tree when overwriting a directory or replacing a file with a directory
if dstStatRes.Info.Type == provider.ResourceType_RESOURCE_TYPE_CONTAINER ||
(dstStatRes.Info.Type == provider.ResourceType_RESOURCE_TYPE_FILE &&
srcStatRes.Info.Type == provider.ResourceType_RESOURCE_TYPE_CONTAINER) {
// we must not allow to override mountpoints - so we check if we have access to the parent. If not this is a mountpoint
if destInShareJail {
res, err := client.GetPath(ctx, &provider.GetPathRequest{ResourceId: dstStatRes.GetInfo().GetId()})
if err != nil || res.GetStatus().GetCode() != rpc.Code_CODE_OK {
log.Error().Err(err).Msg("error sending grpc get path request")
w.WriteHeader(http.StatusInternalServerError)
return nil
}
dir, file := filepath.Split(filepath.Clean(res.GetPath()))
if dir == "/" || dir == "" || file == "" {
log.Error().Msg("must not overwrite mount points")
w.WriteHeader(http.StatusBadRequest)
_, _ = w.Write([]byte("must not overwrite mount points"))
return nil
}
}
delReq := &provider.DeleteRequest{Ref: dstRef}
delRes, err := client.Delete(ctx, delReq)
if err != nil {
log.Error().Err(err).Msg("error sending grpc delete request")
w.WriteHeader(http.StatusInternalServerError)
return nil
}
if delRes.Status.Code != rpc.Code_CODE_OK && delRes.Status.Code != rpc.Code_CODE_NOT_FOUND {
errors.HandleErrorStatus(log, w, delRes.Status)
return nil
}
}
} else if p := path.Dir(dstRef.Path); p != "" {
// check if an intermediate path / the parent exists
pRef := &provider.Reference{
ResourceId: dstRef.ResourceId,
Path: utils.MakeRelativePath(p),
}
intStatReq := &provider.StatRequest{Ref: pRef}
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", pRef).Interface("status", intStatRes.Status).Msg("conflict")
w.WriteHeader(http.StatusConflict)
} else {
errors.HandleErrorStatus(log, w, intStatRes.Status)
}
return nil
}
// TODO what if intermediate is a file?
}
return &copy{source: srcRef, sourceInfo: srcStatRes.Info, depth: depth, successCode: successCode, destination: dstRef}
}
@@ -0,0 +1,454 @@
// Copyright 2018-2021 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 (
"context"
"fmt"
"net/http"
"path"
"path/filepath"
"strings"
gatewayv1beta1 "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
link "github.com/cs3org/go-cs3apis/cs3/sharing/link/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/config"
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/errors"
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/net"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/opencloud-eu/reva/v2/pkg/rhttp/router"
"github.com/opencloud-eu/reva/v2/pkg/storage/utils/grants"
"github.com/opencloud-eu/reva/v2/pkg/storagespace"
"github.com/opencloud-eu/reva/v2/pkg/utils"
"go.opentelemetry.io/otel/trace"
"google.golang.org/grpc/metadata"
)
const (
_trashbinPath = "trash-bin"
// WwwAuthenticate captures the Www-Authenticate header string.
WwwAuthenticate = "Www-Authenticate"
)
const (
ErrListingMembers = "ERR_LISTING_MEMBERS_NOT_ALLOWED"
ErrInvalidCredentials = "ERR_INVALID_CREDENTIALS"
ErrMissingBasicAuth = "ERR_MISSING_BASIC_AUTH"
ErrMissingBearerAuth = "ERR_MISSING_BEARER_AUTH"
ErrFileNotFoundInRoot = "ERR_FILE_NOT_FOUND_IN_ROOT"
)
// DavHandler routes to the different sub handlers
type DavHandler struct {
AvatarsHandler *AvatarsHandler
FilesHandler *WebDavHandler
FilesHomeHandler *WebDavHandler
MetaHandler *MetaHandler
TrashbinHandler *TrashbinHandler
SpacesHandler *SpacesHandler
PublicFolderHandler *WebDavHandler
PublicFileHandler *PublicFileHandler
SharesHandler *WebDavHandler
OCMSharesHandler *WebDavHandler
}
func (h *DavHandler) init(c *config.Config) error {
h.AvatarsHandler = new(AvatarsHandler)
if err := h.AvatarsHandler.init(c); err != nil {
return err
}
h.FilesHandler = new(WebDavHandler)
if err := h.FilesHandler.init(c.FilesNamespace, false); err != nil {
return err
}
h.FilesHomeHandler = new(WebDavHandler)
if err := h.FilesHomeHandler.init(c.WebdavNamespace, true); err != nil {
return err
}
h.MetaHandler = new(MetaHandler)
if err := h.MetaHandler.init(c); err != nil {
return err
}
h.TrashbinHandler = new(TrashbinHandler)
if err := h.TrashbinHandler.init(c); err != nil {
return err
}
h.SpacesHandler = new(SpacesHandler)
if err := h.SpacesHandler.init(c); err != nil {
return err
}
h.PublicFolderHandler = new(WebDavHandler)
if err := h.PublicFolderHandler.init("public", true); 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
}
h.OCMSharesHandler = new(WebDavHandler)
if err := h.OCMSharesHandler.init(c.OCMNamespace, true); err != nil {
return err
}
return nil
}
func isOwner(userIDorName string, user *userv1beta1.User) bool {
return userIDorName != "" && (userIDorName == user.Id.OpaqueId || strings.EqualFold(userIDorName, user.Username))
}
// Handler handles requests
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)
// if there is no file in the request url we assume the request url is: "/remote.php/dav/files"
// https://github.com/owncloud/core/blob/18475dac812064b21dabcc50f25ef3ffe55691a5/tests/acceptance/features/apiWebdavOperations/propfind.feature
if r.URL.Path == "/files" {
log.Debug().Str("path", r.URL.Path).Msg("method not allowed")
contextUser, ok := ctxpkg.ContextGetUser(ctx)
if ok {
r.URL.Path = path.Join(r.URL.Path, contextUser.Username)
}
if r.Header.Get(net.HeaderDepth) == "" {
w.WriteHeader(http.StatusMethodNotAllowed)
b, err := errors.Marshal(http.StatusMethodNotAllowed, "Listing members of this collection is disabled", "", ErrListingMembers)
if err != nil {
log.Error().Msgf("error marshaling xml response: %s", b)
w.WriteHeader(http.StatusInternalServerError)
return
}
_, err = w.Write(b)
if err != nil {
log.Error().Msgf("error writing xml response: %s", b)
w.WriteHeader(http.StatusInternalServerError)
return
}
return
}
}
var head string
head, r.URL.Path = router.ShiftPath(r.URL.Path)
switch head {
case "avatars":
h.AvatarsHandler.Handler(s).ServeHTTP(w, r)
case "files":
var requestUserID string
var oldPath = r.URL.Path
// detect and check current user in URL
requestUserID, r.URL.Path = router.ShiftPath(r.URL.Path)
// note: some requests like OPTIONS don't forward the user
contextUser, ok := ctxpkg.ContextGetUser(ctx)
if ok && isOwner(requestUserID, contextUser) {
// use home storage handler when user was detected
base := path.Join(ctx.Value(net.CtxKeyBaseURI).(string), "files", requestUserID)
ctx := context.WithValue(ctx, net.CtxKeyBaseURI, base)
r = r.WithContext(ctx)
h.FilesHomeHandler.Handler(s).ServeHTTP(w, r)
} else {
r.URL.Path = oldPath
base := path.Join(ctx.Value(net.CtxKeyBaseURI).(string), "files")
ctx := context.WithValue(ctx, net.CtxKeyBaseURI, base)
r = r.WithContext(ctx)
h.FilesHandler.Handler(s).ServeHTTP(w, r)
}
case "meta":
base := path.Join(ctx.Value(net.CtxKeyBaseURI).(string), "meta")
ctx = context.WithValue(ctx, net.CtxKeyBaseURI, base)
r = r.WithContext(ctx)
h.MetaHandler.Handler(s).ServeHTTP(w, r)
case "ocm":
base := path.Join(ctx.Value(net.CtxKeyBaseURI).(string), "ocm")
ctx := context.WithValue(ctx, net.CtxKeyBaseURI, base)
c, err := s.gatewaySelector.Next()
if err != nil {
w.WriteHeader(http.StatusNotFound)
return
}
// OC10 and Nextcloud (OCM 1.0) are using basic auth for carrying the
// ocm share id.
var ocmshare, sharedSecret string
username, _, ok := r.BasicAuth()
if ok {
// OCM 1.0
ocmshare = username
sharedSecret = username
r.URL.Path = filepath.Join("/", ocmshare, r.URL.Path)
} else {
ocmshare, _ = router.ShiftPath(r.URL.Path)
sharedSecret = strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")
}
authRes, err := handleOCMAuth(ctx, c, ocmshare, sharedSecret)
switch {
case err != nil:
log.Error().Err(err).Msg("error during ocm authentication")
w.WriteHeader(http.StatusInternalServerError)
return
case authRes.Status.Code == rpc.Code_CODE_PERMISSION_DENIED:
log.Debug().Str("ocmshare", ocmshare).Msg("permission denied")
fallthrough
case authRes.Status.Code == rpc.Code_CODE_UNAUTHENTICATED:
log.Debug().Str("ocmshare", ocmshare).Msg("unauthorized")
w.WriteHeader(http.StatusUnauthorized)
return
case authRes.Status.Code == rpc.Code_CODE_NOT_FOUND:
log.Debug().Str("ocmshare", ocmshare).Msg("not found")
w.WriteHeader(http.StatusNotFound)
return
case authRes.Status.Code != rpc.Code_CODE_OK:
log.Error().Str("ocmshare", ocmshare).Interface("status", authRes.Status).Msg("grpc auth request failed")
w.WriteHeader(http.StatusInternalServerError)
return
}
ctx = ctxpkg.ContextSetToken(ctx, authRes.Token)
ctx = ctxpkg.ContextSetUser(ctx, authRes.User)
ctx = metadata.AppendToOutgoingContext(ctx, ctxpkg.TokenHeader, authRes.Token)
log.Debug().Str("ocmshare", ocmshare).Interface("user", authRes.User).Msg("OCM user authenticated")
r = r.WithContext(ctx)
h.OCMSharesHandler.Handler(s).ServeHTTP(w, r)
case "trash-bin":
base := path.Join(ctx.Value(net.CtxKeyBaseURI).(string), "trash-bin")
ctx := context.WithValue(ctx, net.CtxKeyBaseURI, base)
r = r.WithContext(ctx)
h.TrashbinHandler.Handler(s).ServeHTTP(w, r)
case "spaces":
base := path.Join(ctx.Value(net.CtxKeyBaseURI).(string), "spaces")
ctx := context.WithValue(ctx, net.CtxKeyBaseURI, base)
r = r.WithContext(ctx)
h.SpacesHandler.Handler(s, h.TrashbinHandler).ServeHTTP(w, r)
case "public-files":
base := path.Join(ctx.Value(net.CtxKeyBaseURI).(string), "public-files")
ctx = context.WithValue(ctx, net.CtxKeyBaseURI, base)
var res *gatewayv1beta1.AuthenticateResponse
token, _ := router.ShiftPath(r.URL.Path)
var hasValidBasicAuthHeader bool
var pass string
var err error
// If user is authenticated
_, userExists := ctxpkg.ContextGetUser(ctx)
if userExists {
client, err := s.gatewaySelector.Next()
if err != nil {
log.Error().Err(err).Msg("error sending grpc stat request")
w.WriteHeader(http.StatusInternalServerError)
return
}
psRes, err := client.GetPublicShare(ctx, &link.GetPublicShareRequest{
Ref: &link.PublicShareReference{
Spec: &link.PublicShareReference_Token{
Token: token,
},
}})
if err != nil && !strings.Contains(err.Error(), "core access token not found") {
log.Error().Err(err).Msg("error sending grpc stat request")
w.WriteHeader(http.StatusInternalServerError)
return
}
// If the link is internal then 307 redirect
if psRes.Status.Code == rpc.Code_CODE_OK && grants.PermissionsEqual(psRes.Share.Permissions.GetPermissions(), &provider.ResourcePermissions{}) {
if psRes.GetShare().GetResourceId() != nil {
rUrl := path.Join("/dav/spaces", storagespace.FormatResourceID(psRes.GetShare().GetResourceId()))
http.Redirect(w, r, rUrl, http.StatusTemporaryRedirect)
return
}
log.Debug().Str("token", token).Interface("status", psRes.Status).Msg("resource id not found")
w.WriteHeader(http.StatusNotFound)
return
}
}
if _, pass, hasValidBasicAuthHeader = r.BasicAuth(); hasValidBasicAuthHeader {
res, err = handleBasicAuth(r.Context(), s.gatewaySelector, token, pass)
} else {
q := r.URL.Query()
sig := q.Get("signature")
expiration := q.Get("expiration")
// We restrict the pre-signed urls to downloads.
if sig != "" && expiration != "" && !(r.Method == http.MethodGet || r.Method == http.MethodHead) {
w.WriteHeader(http.StatusUnauthorized)
return
}
res, err = handleSignatureAuth(r.Context(), s.gatewaySelector, token, sig, expiration)
}
switch {
case err != nil:
w.WriteHeader(http.StatusInternalServerError)
return
case res.Status.Code == rpc.Code_CODE_PERMISSION_DENIED:
fallthrough
case res.Status.Code == rpc.Code_CODE_UNAUTHENTICATED:
w.WriteHeader(http.StatusUnauthorized)
if hasValidBasicAuthHeader {
b, err := errors.Marshal(http.StatusUnauthorized, "Username or password was incorrect", "", ErrInvalidCredentials)
errors.HandleWebdavError(log, w, b, err)
return
}
b, err := errors.Marshal(http.StatusUnauthorized, "No 'Authorization: Basic' header found", "", ErrMissingBasicAuth)
errors.HandleWebdavError(log, w, b, err)
return
case res.Status.Code == rpc.Code_CODE_NOT_FOUND:
w.WriteHeader(http.StatusNotFound)
return
case res.Status.Code != rpc.Code_CODE_OK:
w.WriteHeader(http.StatusInternalServerError)
return
}
if userExists {
// Build new context without an authenticated user.
// the public link should be resolved by the 'publicshares' authenticated user
baseURI := ctx.Value(net.CtxKeyBaseURI).(string)
logger := appctx.GetLogger(ctx)
span := trace.SpanFromContext(ctx)
span.End()
ctx = trace.ContextWithSpan(context.Background(), span)
ctx = appctx.WithLogger(ctx, logger)
ctx = context.WithValue(ctx, net.CtxKeyBaseURI, baseURI)
}
ctx = ctxpkg.ContextSetToken(ctx, res.Token)
ctx = ctxpkg.ContextSetUser(ctx, res.User)
ctx = metadata.AppendToOutgoingContext(ctx, ctxpkg.TokenHeader, res.Token)
r = r.WithContext(ctx)
// the public share manager knew the token, but does the referenced target still exist?
sRes, err := getTokenStatInfo(ctx, s.gatewaySelector, token)
switch {
case err != nil:
log.Error().Err(err).Msg("error sending grpc stat request")
w.WriteHeader(http.StatusInternalServerError)
return
case sRes.Status.Code == rpc.Code_CODE_PERMISSION_DENIED:
fallthrough
case sRes.Status.Code == rpc.Code_CODE_OK && grants.PermissionsEqual(sRes.GetInfo().GetPermissionSet(), &provider.ResourcePermissions{}):
// If the link is internal
if !userExists {
w.Header().Add(WwwAuthenticate, fmt.Sprintf("Bearer realm=\"%s\", charset=\"UTF-8\"", r.Host))
w.WriteHeader(http.StatusUnauthorized)
b, err := errors.Marshal(http.StatusUnauthorized, "No 'Authorization: Bearer' header found", "", ErrMissingBearerAuth)
errors.HandleWebdavError(log, w, b, err)
return
}
fallthrough
case sRes.Status.Code == rpc.Code_CODE_NOT_FOUND:
log.Debug().Str("token", token).Interface("status", res.Status).Msg("resource not found")
w.WriteHeader(http.StatusNotFound) // log the difference
return
case sRes.Status.Code == rpc.Code_CODE_UNAUTHENTICATED:
log.Debug().Str("token", token).Interface("status", res.Status).Msg("unauthorized")
w.WriteHeader(http.StatusUnauthorized)
return
case sRes.Status.Code != rpc.Code_CODE_OK:
log.Error().Str("token", token).Interface("status", res.Status).Msg("grpc stat request failed")
w.WriteHeader(http.StatusInternalServerError)
return
}
log.Debug().Interface("statInfo", sRes.Info).Msg("Stat info from public link token path")
ctx := ContextWithTokenStatInfo(ctx, sRes.Info)
r = r.WithContext(ctx)
if sRes.Info.Type != provider.ResourceType_RESOURCE_TYPE_CONTAINER {
h.PublicFileHandler.Handler(s).ServeHTTP(w, r)
} else {
h.PublicFolderHandler.Handler(s).ServeHTTP(w, r)
}
default:
w.WriteHeader(http.StatusNotFound)
b, err := errors.Marshal(http.StatusNotFound, "File not found in root", "", ErrFileNotFoundInRoot)
errors.HandleWebdavError(log, w, b, err)
}
})
}
func getTokenStatInfo(ctx context.Context, selector pool.Selectable[gatewayv1beta1.GatewayAPIClient], token string) (*provider.StatResponse, error) {
client, err := selector.Next()
if err != nil {
return nil, err
}
return client.Stat(ctx, &provider.StatRequest{Ref: &provider.Reference{
ResourceId: &provider.ResourceId{
StorageId: utils.PublicStorageProviderID,
SpaceId: utils.PublicStorageSpaceID,
OpaqueId: token,
},
}})
}
func handleBasicAuth(ctx context.Context, selector pool.Selectable[gatewayv1beta1.GatewayAPIClient], token, pw string) (*gatewayv1beta1.AuthenticateResponse, error) {
c, err := selector.Next()
if err != nil {
return nil, err
}
authenticateRequest := gatewayv1beta1.AuthenticateRequest{
Type: "publicshares",
ClientId: token,
ClientSecret: "password|" + pw,
}
return c.Authenticate(ctx, &authenticateRequest)
}
func handleSignatureAuth(ctx context.Context, selector pool.Selectable[gatewayv1beta1.GatewayAPIClient], token, sig, expiration string) (*gatewayv1beta1.AuthenticateResponse, error) {
c, err := selector.Next()
if err != nil {
return nil, err
}
authenticateRequest := gatewayv1beta1.AuthenticateRequest{
Type: "publicshares",
ClientId: token,
ClientSecret: "signature|" + sig + "|" + expiration,
}
return c.Authenticate(ctx, &authenticateRequest)
}
func handleOCMAuth(ctx context.Context, c gatewayv1beta1.GatewayAPIClient, ocmshare, sharedSecret string) (*gatewayv1beta1.AuthenticateResponse, error) {
return c.Authenticate(ctx, &gatewayv1beta1.AuthenticateRequest{
Type: "ocmshares",
ClientId: ocmshare,
ClientSecret: sharedSecret,
})
}
@@ -0,0 +1,149 @@
// Copyright 2018-2021 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 (
"context"
"errors"
"net/http"
"path"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/net"
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/spacelookup"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
rstatus "github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
"github.com/opencloud-eu/reva/v2/pkg/utils"
)
func (s *svc) handlePathDelete(w http.ResponseWriter, r *http.Request, ns string) (status int, err error) {
ctx := r.Context()
ctx, span := appctx.GetTracerProvider(r.Context()).Tracer(tracerName).Start(ctx, "path_delete")
defer span.End()
if r.Body != http.NoBody {
return http.StatusUnsupportedMediaType, errors.New("body must be empty")
}
fn := path.Join(ns, r.URL.Path)
space, rpcStatus, err := spacelookup.LookUpStorageSpaceForPath(ctx, s.gatewaySelector, fn)
switch {
case err != nil:
span.RecordError(err)
return http.StatusInternalServerError, err
case rpcStatus.Code != rpc.Code_CODE_OK:
return rstatus.HTTPStatusFromCode(rpcStatus.Code), errtypes.NewErrtypeFromStatus(rpcStatus)
}
return s.handleDelete(ctx, w, r, spacelookup.MakeRelativeReference(space, fn, false))
}
func (s *svc) handleDelete(ctx context.Context, w http.ResponseWriter, r *http.Request, ref *provider.Reference) (status int, err error) {
ctx, span := appctx.GetTracerProvider(r.Context()).Tracer(tracerName).Start(ctx, "delete")
defer span.End()
req := &provider.DeleteRequest{
Ref: ref,
LockId: requestLockToken(r),
}
// FIXME the lock token is part of the application level protocol, it should be part of the DeleteRequest message not the opaque
ih, ok := parseIfHeader(r.Header.Get(net.HeaderIf))
if ok {
if len(ih.lists) == 1 && len(ih.lists[0].conditions) == 1 {
req.Opaque = utils.AppendPlainToOpaque(req.Opaque, "lockid", ih.lists[0].conditions[0].Token)
}
} else if r.Header.Get(net.HeaderIf) != "" {
return http.StatusBadRequest, errtypes.BadRequest("invalid if header")
}
client, err := s.gatewaySelector.Next()
if err != nil {
return http.StatusInternalServerError, errtypes.InternalError(err.Error())
}
res, err := client.Delete(ctx, req)
switch {
case err != nil:
span.RecordError(err)
return http.StatusInternalServerError, err
case res.Status.Code == rpc.Code_CODE_OK:
return http.StatusNoContent, nil
case res.Status.Code == rpc.Code_CODE_NOT_FOUND:
//lint:ignore ST1005 mimic the exact oc10 error message
return http.StatusNotFound, errors.New("Resource not found")
case res.Status.Code == rpc.Code_CODE_PERMISSION_DENIED:
status = http.StatusForbidden
if lockID := utils.ReadPlainFromOpaque(res.Opaque, "lockid"); lockID != "" {
// http://www.webdav.org/specs/rfc4918.html#HEADER_Lock-Token says that the
// Lock-Token value is a Coded-URL. We add angle brackets.
w.Header().Set("Lock-Token", "<"+lockID+">")
status = http.StatusLocked
}
// check if user has access to resource
sRes, err := client.Stat(ctx, &provider.StatRequest{Ref: ref})
if err != nil {
span.RecordError(err)
return http.StatusInternalServerError, err
}
if sRes.Status.Code != rpc.Code_CODE_OK {
// return not found error so we do not leak existence of a file
// TODO hide permission failed for users without access in every kind of request
// TODO should this be done in the driver?
//lint:ignore ST1005 mimic the exact oc10 error message
return http.StatusNotFound, errors.New("Resource not found")
}
return status, errors.New("") // mimic the oc10 error messages which have an empty message in this case
case res.Status.Code == rpc.Code_CODE_INTERNAL && res.Status.Message == "can't delete mount path":
// 405 must generate an Allow header
w.Header().Set("Allow", "PROPFIND, MOVE, COPY, POST, PROPPATCH, HEAD, GET, OPTIONS, LOCK, UNLOCK, REPORT, SEARCH, PUT")
return http.StatusMethodNotAllowed, errtypes.PermissionDenied(res.Status.Message)
}
return rstatus.HTTPStatusFromCode(res.Status.Code), errtypes.NewErrtypeFromStatus(res.Status)
}
func (s *svc) handleSpacesDelete(w http.ResponseWriter, r *http.Request, spaceID string) (status int, err error) {
ctx := r.Context()
ctx, span := appctx.GetTracerProvider(r.Context()).Tracer(tracerName).Start(ctx, "spaces_delete")
defer span.End()
if r.Body != http.NoBody {
return http.StatusUnsupportedMediaType, errors.New("body must be empty")
}
ref, err := spacelookup.MakeStorageSpaceReference(spaceID, r.URL.Path)
if err != nil {
return http.StatusBadRequest, err
}
// do not allow deleting spaces via dav endpoint - use graph endpoint instead
// we get a relative reference coming from the space root
// so if the path is "empty" and no opaque id is present or the opaque id equals
// the space id, we are referencing the space
rid := ref.GetResourceId()
if ref.GetPath() == "." &&
(rid.GetOpaqueId() == "" || rid.GetOpaqueId() == rid.GetSpaceId()) {
return http.StatusMethodNotAllowed, errors.New("deleting spaces via dav is not allowed")
}
return s.handleDelete(ctx, w, r, &ref)
}
@@ -0,0 +1,214 @@
// Copyright 2018-2021 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 errors
import (
"bytes"
"encoding/xml"
"net/http"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
"github.com/pkg/errors"
"github.com/rs/zerolog"
)
var sabreException = map[int]string{
// the commented states have no corresponding exception in sabre/dav,
// see https://github.com/sabre-io/dav/tree/master/lib/DAV/Exception
// http.StatusMultipleChoices: "Multiple Choices",
// http.StatusMovedPermanently: "Moved Permanently",
// http.StatusFound: "Found",
// http.StatusSeeOther: "See Other",
// http.StatusNotModified: "Not Modified",
// http.StatusUseProxy: "Use Proxy",
// http.StatusTemporaryRedirect: "Temporary Redirect",
// http.StatusPermanentRedirect: "Permanent Redirect",
http.StatusBadRequest: "Sabre\\DAV\\Exception\\BadRequest",
http.StatusUnauthorized: "Sabre\\DAV\\Exception\\NotAuthenticated",
http.StatusPaymentRequired: "Sabre\\DAV\\Exception\\PaymentRequired",
http.StatusForbidden: "Sabre\\DAV\\Exception\\Forbidden", // InvalidResourceType, InvalidSyncToken, TooManyMatches
http.StatusNotFound: "Sabre\\DAV\\Exception\\NotFound",
http.StatusMethodNotAllowed: "Sabre\\DAV\\Exception\\MethodNotAllowed",
// http.StatusNotAcceptable: "Not Acceptable",
// http.StatusProxyAuthRequired: "Proxy Authentication Required",
// http.StatusRequestTimeout: "Request Timeout",
http.StatusConflict: "Sabre\\DAV\\Exception\\Conflict", // LockTokenMatchesRequestUri
// http.StatusGone: "Gone",
http.StatusLengthRequired: "Sabre\\DAV\\Exception\\LengthRequired",
http.StatusPreconditionFailed: "Sabre\\DAV\\Exception\\PreconditionFailed",
// http.StatusRequestEntityTooLarge: "Request Entity Too Large",
// http.StatusRequestURITooLong: "Request URI Too Long",
http.StatusUnsupportedMediaType: "Sabre\\DAV\\Exception\\UnsupportedMediaType", // ReportNotSupported
http.StatusRequestedRangeNotSatisfiable: "Sabre\\DAV\\Exception\\RequestedRangeNotSatisfiable",
// http.StatusExpectationFailed: "Expectation Failed",
// http.StatusTeapot: "I'm a teapot",
// http.StatusMisdirectedRequest: "Misdirected Request",
// http.StatusUnprocessableEntity: "Unprocessable Entity",
http.StatusLocked: "Sabre\\DAV\\Exception\\Locked", // ConflictingLock
// http.StatusFailedDependency: "Failed Dependency",
// http.StatusTooEarly: "Too Early",
// http.StatusUpgradeRequired: "Upgrade Required",
// http.StatusPreconditionRequired: "Precondition Required",
// http.StatusTooManyRequests: "Too Many Requests",
// http.StatusRequestHeaderFieldsTooLarge: "Request Header Fields Too Large",
// http.StatusUnavailableForLegalReasons: "Unavailable For Legal Reasons",
// http.StatusInternalServerError: "Internal Server Error",
http.StatusNotImplemented: "Sabre\\DAV\\Exception\\NotImplemented",
// http.StatusBadGateway: "Bad Gateway",
http.StatusServiceUnavailable: "Sabre\\DAV\\Exception\\ServiceUnavailable",
// http.StatusGatewayTimeout: "Gateway Timeout",
// http.StatusHTTPVersionNotSupported: "HTTP Version Not Supported",
// http.StatusVariantAlsoNegotiates: "Variant Also Negotiates",
http.StatusInsufficientStorage: "Sabre\\DAV\\Exception\\InsufficientStorage",
// http.StatusLoopDetected: "Loop Detected",
// http.StatusNotExtended: "Not Extended",
// http.StatusNetworkAuthenticationRequired: "Network Authentication Required",
}
// SabreException returns a sabre exception text for the HTTP status code. It returns the empty
// string if the code is unknown.
func SabreException(code int) string {
return sabreException[code]
}
// Exception represents a ocdav exception
type Exception struct {
Code int
Message string
Header string
}
// Marshal just calls the xml marshaller for a given exception.
func Marshal(code int, message string, header string, errorCode string) ([]byte, error) {
xmlstring, err := xml.Marshal(&ErrorXML{
Xmlnsd: "DAV",
Xmlnss: "http://sabredav.org/ns",
Exception: sabreException[code],
Message: message,
Header: header,
ErrorCode: errorCode,
})
if err != nil {
return nil, err
}
var buf bytes.Buffer
buf.WriteString(xml.Header)
buf.Write(xmlstring)
return buf.Bytes(), err
}
// ErrorXML holds the xml representation of an error
// http://www.webdav.org/specs/rfc4918.html#ELEMENT_error
type ErrorXML struct {
XMLName xml.Name `xml:"d:error"`
Xmlnsd string `xml:"xmlns:d,attr"`
Xmlnss string `xml:"xmlns:s,attr"`
Exception string `xml:"s:exception"`
Message string `xml:"s:message"`
ErrorCode string `xml:"s:errorcode"`
InnerXML []byte `xml:",innerxml"`
// Header is used to indicate the conflicting request header
Header string `xml:"s:header,omitempty"`
}
var (
// ErrInvalidDepth is an invalid depth header error
ErrInvalidDepth = errors.New("webdav: invalid depth")
// ErrInvalidPropfind is an invalid propfind error
ErrInvalidPropfind = errors.New("webdav: invalid propfind")
// ErrInvalidProppatch is an invalid proppatch error
ErrInvalidProppatch = errors.New("webdav: invalid proppatch")
// ErrInvalidLockInfo is an invalid lock error
ErrInvalidLockInfo = errors.New("webdav: invalid lock info")
// ErrUnsupportedLockInfo is an unsupported lock error
ErrUnsupportedLockInfo = errors.New("webdav: unsupported lock info")
// ErrInvalidTimeout is an invalid timeout error
ErrInvalidTimeout = errors.New("webdav: invalid timeout")
// ErrInvalidIfHeader is an invalid if header error
ErrInvalidIfHeader = errors.New("webdav: invalid If header")
// ErrUnsupportedMethod is an unsupported method error
ErrUnsupportedMethod = errors.New("webdav: unsupported method")
// ErrInvalidLockToken is an invalid lock token error
ErrInvalidLockToken = errors.New("webdav: invalid lock token")
// ErrConfirmationFailed is returned by a LockSystem's Confirm method.
ErrConfirmationFailed = errors.New("webdav: confirmation failed")
// ErrForbidden is returned by a LockSystem's Unlock method.
ErrForbidden = errors.New("webdav: forbidden")
// ErrLocked is returned by a LockSystem's Create, Refresh and Unlock methods.
ErrLocked = errors.New("webdav: locked")
// ErrNoSuchLock is returned by a LockSystem's Refresh and Unlock methods.
ErrNoSuchLock = errors.New("webdav: no such lock")
// ErrNotImplemented is returned when hitting not implemented code paths
ErrNotImplemented = errors.New("webdav: not implemented")
// ErrTokenNotFound is returned when a token is not found
ErrTokenStatInfoMissing = errors.New("webdav: token stat info missing")
)
// HandleErrorStatus checks the status code, logs a Debug or Error level message
// and writes an appropriate http status
func HandleErrorStatus(log *zerolog.Logger, w http.ResponseWriter, s *rpc.Status) {
hsc := status.HTTPStatusFromCode(s.Code)
if s.Code == rpc.Code_CODE_ABORTED {
// aborted is used for etag an lock mismatches, which translates to 412
// in case a real Conflict response is needed, the calling code needs to send the header
hsc = http.StatusPreconditionFailed
}
if hsc == http.StatusInternalServerError {
log.Error().Interface("status", s).Int("code", hsc).Msg(http.StatusText(hsc))
} else {
log.Debug().Interface("status", s).Int("code", hsc).Msg(http.StatusText(hsc))
}
w.WriteHeader(hsc)
}
// HandleWebdavError checks the status code, logs an error and creates a webdav response body
// if needed
func HandleWebdavError(log *zerolog.Logger, w http.ResponseWriter, b []byte, err error) {
if err != nil {
log.Error().Msgf("error marshaling xml response: %s", b)
w.WriteHeader(http.StatusInternalServerError)
return
}
_, err = w.Write(b)
if err != nil {
log.Err(err).Msg("error writing response")
}
}
func NewErrFromStatus(s *rpc.Status) error {
switch s.GetCode() {
case rpc.Code_CODE_OK:
return nil
case rpc.Code_CODE_DEADLINE_EXCEEDED:
return ErrInvalidTimeout
case rpc.Code_CODE_PERMISSION_DENIED:
return ErrForbidden
case rpc.Code_CODE_LOCKED, rpc.Code_CODE_FAILED_PRECONDITION:
return ErrLocked
case rpc.Code_CODE_UNIMPLEMENTED:
return ErrNotImplemented
default:
return errors.New(s.GetMessage())
}
}
@@ -0,0 +1,47 @@
package ocdav
import (
"context"
"errors"
"path/filepath"
"strconv"
"strings"
gatewayv1beta1 "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
)
// FindName returns the next filename available when the current
func FindName(ctx context.Context, client gatewayv1beta1.GatewayAPIClient, name string, parentid *provider.ResourceId) (string, *rpc.Status, error) {
lReq := &provider.ListContainerRequest{
Ref: &provider.Reference{
ResourceId: parentid,
},
}
lRes, err := client.ListContainer(ctx, lReq)
if err != nil {
return "", nil, err
}
if lRes.Status.Code != rpc.Code_CODE_OK {
return "", lRes.Status, nil
}
// iterate over the listing to determine next suffix
var itemMap = make(map[string]struct{})
for _, fi := range lRes.Infos {
itemMap[fi.GetName()] = struct{}{}
}
ext := filepath.Ext(name)
fileName := strings.TrimSuffix(name, ext)
if strings.HasSuffix(fileName, ".tar") {
fileName = strings.TrimSuffix(fileName, ".tar")
ext = filepath.Ext(fileName) + "." + ext
}
// starts with two because "normal" humans begin counting with 1 and we say the existing file is the first one
for i := 2; i < len(itemMap)+3; i++ {
if _, ok := itemMap[fileName+" ("+strconv.Itoa(i)+")"+ext]; !ok {
return fileName + " (" + strconv.Itoa(i) + ")" + ext, lRes.GetStatus(), nil
}
}
return "", nil, errors.New("could not determine new filename")
}
@@ -0,0 +1,190 @@
// Copyright 2018-2021 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 (
"context"
"io"
"net/http"
"path"
"strconv"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/opencloud-eu/reva/v2/internal/http/services/datagateway"
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/errors"
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/net"
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/spacelookup"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
"github.com/opencloud-eu/reva/v2/pkg/rhttp"
"github.com/opencloud-eu/reva/v2/pkg/utils"
"github.com/rs/zerolog"
)
func (s *svc) handlePathGet(w http.ResponseWriter, r *http.Request, ns string) {
ctx, span := appctx.GetTracerProvider(r.Context()).Tracer(tracerName).Start(r.Context(), "get")
defer span.End()
fn := path.Join(ns, r.URL.Path)
sublog := appctx.GetLogger(ctx).With().Str("path", fn).Str("svc", "ocdav").Str("handler", "get").Logger()
space, status, err := spacelookup.LookUpStorageSpaceForPath(ctx, s.gatewaySelector, fn)
if err != nil {
sublog.Error().Err(err).Str("path", fn).Msg("failed to look up storage space")
w.WriteHeader(http.StatusInternalServerError)
return
}
if status.Code != rpc.Code_CODE_OK {
errors.HandleErrorStatus(&sublog, w, status)
return
}
s.handleGet(ctx, w, r, spacelookup.MakeRelativeReference(space, fn, false), "spaces", 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.gatewaySelector.Next()
if err != nil {
log.Error().Err(err).Msg("error selecting next client")
w.WriteHeader(http.StatusInternalServerError)
return
}
sReq := &provider.StatRequest{
Ref: ref,
}
sRes, err := client.Stat(ctx, sReq)
if err != nil {
log.Error().Err(err).Msg("error stat resource")
w.WriteHeader(http.StatusInternalServerError)
return
} else if sRes.Status.Code != rpc.Code_CODE_OK {
errors.HandleErrorStatus(&log, w, sRes.Status)
return
}
if sRes.Info.Type != provider.ResourceType_RESOURCE_TYPE_FILE {
w.Header().Set("Content-Length", "0")
w.WriteHeader(http.StatusOK)
return
}
if status := utils.ReadPlainFromOpaque(sRes.GetInfo().GetOpaque(), "status"); status == "processing" {
w.WriteHeader(http.StatusTooEarly)
return
}
dReq := &provider.InitiateFileDownloadRequest{Ref: ref}
dRes, err := client.InitiateFileDownload(ctx, dReq)
switch {
case err != nil:
log.Error().Err(err).Msg("error initiating file download")
w.WriteHeader(http.StatusInternalServerError)
return
case dRes.Status.Code != rpc.Code_CODE_OK:
errors.HandleErrorStatus(&log, w, dRes.Status)
return
}
var ep, token string
for _, p := range dRes.Protocols {
if p.Protocol == dlProtocol {
ep, token = p.DownloadEndpoint, p.Token
}
}
httpReq, err := rhttp.NewRequest(ctx, http.MethodGet, ep, nil)
if err != nil {
log.Error().Err(err).Msg("error creating http request")
w.WriteHeader(http.StatusInternalServerError)
return
}
httpReq.Header.Set(datagateway.TokenTransportHeader, token)
if r.Header.Get(net.HeaderRange) != "" {
httpReq.Header.Set(net.HeaderRange, r.Header.Get(net.HeaderRange))
}
httpClient := s.client
httpRes, err := httpClient.Do(httpReq)
if err != nil {
log.Error().Err(err).Msg("error performing http request")
w.WriteHeader(http.StatusInternalServerError)
return
}
defer httpRes.Body.Close()
// copy only the headers relevant for the content served by the datagateway
// more headers are already present from the GET request
copyHeader(w.Header(), httpRes.Header, net.HeaderContentType)
copyHeader(w.Header(), httpRes.Header, net.HeaderContentLength)
copyHeader(w.Header(), httpRes.Header, net.HeaderContentRange)
copyHeader(w.Header(), httpRes.Header, net.HeaderOCFileID)
copyHeader(w.Header(), httpRes.Header, net.HeaderOCETag)
copyHeader(w.Header(), httpRes.Header, net.HeaderOCChecksum)
copyHeader(w.Header(), httpRes.Header, net.HeaderETag)
copyHeader(w.Header(), httpRes.Header, net.HeaderLastModified)
copyHeader(w.Header(), httpRes.Header, net.HeaderAcceptRanges)
copyHeader(w.Header(), httpRes.Header, net.HeaderContentDisposistion)
w.WriteHeader(httpRes.StatusCode)
if httpRes.StatusCode != http.StatusOK && httpRes.StatusCode != http.StatusPartialContent {
// swallow the body and set content-length to 0 to prevent reverse proxies from trying to read from it
w.Header().Set("Content-Length", "0")
return
}
var c int64
if c, err = io.Copy(w, httpRes.Body); err != nil {
log.Error().Err(err).Msg("error finishing copying data to response")
}
if httpRes.Header.Get(net.HeaderContentLength) != "" {
i, err := strconv.ParseInt(httpRes.Header.Get(net.HeaderContentLength), 10, 64)
if err != nil {
log.Error().Err(err).Str("content-length", httpRes.Header.Get(net.HeaderContentLength)).Msg("invalid content length in datagateway response")
}
if i != c {
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
}
func copyHeader(dist, src http.Header, header string) {
if src.Get(header) != "" {
dist.Set(header, src.Get(header))
}
}
func (s *svc) handleSpacesGet(w http.ResponseWriter, r *http.Request, spaceID string) {
ctx, span := appctx.GetTracerProvider(r.Context()).Tracer(tracerName).Start(r.Context(), "spaces_get")
defer span.End()
sublog := appctx.GetLogger(ctx).With().Str("path", r.URL.Path).Str("spaceid", spaceID).Str("handler", "get").Logger()
ref, err := spacelookup.MakeStorageSpaceReference(spaceID, r.URL.Path)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
s.handleGet(ctx, w, r, &ref, "spaces", sublog)
}
@@ -0,0 +1,120 @@
// Copyright 2018-2021 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 (
"context"
"fmt"
"net/http"
"path"
"strconv"
"strings"
"time"
"github.com/opencloud-eu/reva/v2/pkg/storagespace"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/opencloud-eu/reva/v2/internal/grpc/services/storageprovider"
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/errors"
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/net"
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/spacelookup"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
"github.com/opencloud-eu/reva/v2/pkg/utils"
"github.com/rs/zerolog"
)
func (s *svc) handlePathHead(w http.ResponseWriter, r *http.Request, ns string) {
ctx, span := appctx.GetTracerProvider(r.Context()).Tracer(tracerName).Start(r.Context(), "head")
defer span.End()
fn := path.Join(ns, r.URL.Path)
sublog := appctx.GetLogger(ctx).With().Str("path", fn).Logger()
space, status, err := spacelookup.LookUpStorageSpaceForPath(ctx, s.gatewaySelector, fn)
if err != nil {
sublog.Error().Err(err).Str("path", fn).Msg("failed to look up storage space")
w.WriteHeader(http.StatusInternalServerError)
return
}
if status.Code != rpc.Code_CODE_OK {
errors.HandleErrorStatus(&sublog, w, status)
return
}
s.handleHead(ctx, w, r, spacelookup.MakeRelativeReference(space, fn, false), sublog)
}
func (s *svc) handleHead(ctx context.Context, w http.ResponseWriter, r *http.Request, ref *provider.Reference, log zerolog.Logger) {
client, err := s.gatewaySelector.Next()
if err != nil {
log.Error().Err(err).Msg("error selecting next client")
w.WriteHeader(http.StatusInternalServerError)
return
}
req := &provider.StatRequest{Ref: ref}
res, err := client.Stat(ctx, req)
if err != nil {
log.Error().Err(err).Msg("error sending grpc stat request")
w.WriteHeader(http.StatusInternalServerError)
return
}
if res.Status.Code != rpc.Code_CODE_OK {
errors.HandleErrorStatus(&log, w, res.Status)
return
}
info := res.Info
w.Header().Set(net.HeaderContentType, info.MimeType)
w.Header().Set(net.HeaderETag, info.Etag)
w.Header().Set(net.HeaderOCFileID, storagespace.FormatResourceID(info.Id))
w.Header().Set(net.HeaderOCETag, info.Etag)
if info.Checksum != nil {
w.Header().Set(net.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(net.HeaderLastModified, lastModifiedString)
w.Header().Set(net.HeaderContentLength, strconv.FormatUint(info.Size, 10))
if info.Type != provider.ResourceType_RESOURCE_TYPE_CONTAINER {
w.Header().Set(net.HeaderAcceptRanges, "bytes")
}
if utils.ReadPlainFromOpaque(res.GetInfo().GetOpaque(), "status") == "processing" {
w.WriteHeader(http.StatusTooEarly)
return
}
w.WriteHeader(http.StatusOK)
}
func (s *svc) handleSpacesHead(w http.ResponseWriter, r *http.Request, spaceID string) {
ctx, span := appctx.GetTracerProvider(r.Context()).Tracer(tracerName).Start(r.Context(), "spaces_head")
defer span.End()
sublog := appctx.GetLogger(ctx).With().Str("spaceid", spaceID).Str("path", r.URL.Path).Logger()
ref, err := spacelookup.MakeStorageSpaceReference(spaceID, r.URL.Path)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
s.handleHead(ctx, w, r, &ref, sublog)
}
@@ -0,0 +1,193 @@
// Copyright 2018-2021 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.
// Copyright 2014 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package ocdav
// copy of https://github.com/golang/net/blob/master/webdav/if.go
// The If header is covered by Section 10.4.
// http://www.webdav.org/specs/rfc4918.html#HEADER_If
import (
"strings"
)
// ifHeader is a disjunction (OR) of ifLists.
type ifHeader struct {
lists []ifList
}
// ifList is a conjunction (AND) of Conditions, and an optional resource tag.
type ifList struct {
resourceTag string
conditions []Condition
}
// parseIfHeader parses the "If: foo bar" HTTP header. The httpHeader string
// should omit the "If:" prefix and have any "\r\n"s collapsed to a " ", as is
// returned by req.Header.Get("If") for a http.Request req.
func parseIfHeader(httpHeader string) (h ifHeader, ok bool) {
s := strings.TrimSpace(httpHeader)
switch tokenType, _, _ := lex(s); tokenType {
case '(':
return parseNoTagLists(s)
case angleTokenType:
return parseTaggedLists(s)
default:
return ifHeader{}, false
}
}
func parseNoTagLists(s string) (h ifHeader, ok bool) {
for {
l, remaining, ok := parseList(s)
if !ok {
return ifHeader{}, false
}
h.lists = append(h.lists, l)
if remaining == "" {
return h, true
}
s = remaining
}
}
func parseTaggedLists(s string) (h ifHeader, ok bool) {
resourceTag, n := "", 0
for first := true; ; first = false {
tokenType, tokenStr, remaining := lex(s)
switch tokenType {
case angleTokenType:
if !first && n == 0 {
return ifHeader{}, false
}
resourceTag, n = tokenStr, 0
s = remaining
case '(':
n++
l, remaining, ok := parseList(s)
if !ok {
return ifHeader{}, false
}
l.resourceTag = resourceTag
h.lists = append(h.lists, l)
if remaining == "" {
return h, true
}
s = remaining
default:
return ifHeader{}, false
}
}
}
func parseList(s string) (l ifList, remaining string, ok bool) {
tokenType, _, s := lex(s)
if tokenType != '(' {
return ifList{}, "", false
}
for {
tokenType, _, remaining = lex(s)
if tokenType == ')' {
if len(l.conditions) == 0 {
return ifList{}, "", false
}
return l, remaining, true
}
c, remaining, ok := parseCondition(s)
if !ok {
return ifList{}, "", false
}
l.conditions = append(l.conditions, c)
s = remaining
}
}
func parseCondition(s string) (c Condition, remaining string, ok bool) {
tokenType, tokenStr, s := lex(s)
if tokenType == notTokenType {
c.Not = true
tokenType, tokenStr, s = lex(s)
}
switch tokenType {
case strTokenType, angleTokenType:
c.Token = tokenStr
case squareTokenType:
c.ETag = tokenStr
default:
return Condition{}, "", false
}
return c, s, true
}
// Single-rune tokens like '(' or ')' have a token type equal to their rune.
// All other tokens have a negative token type.
const (
errTokenType = rune(-1)
eofTokenType = rune(-2)
strTokenType = rune(-3)
notTokenType = rune(-4)
angleTokenType = rune(-5)
squareTokenType = rune(-6)
)
func lex(s string) (tokenType rune, tokenStr string, remaining string) {
// The net/textproto Reader that parses the HTTP header will collapse
// Linear White Space that spans multiple "\r\n" lines to a single " ",
// so we don't need to look for '\r' or '\n'.
for len(s) > 0 && (s[0] == '\t' || s[0] == ' ') {
s = s[1:]
}
if len(s) == 0 {
return eofTokenType, "", ""
}
i := 0
loop:
for ; i < len(s); i++ {
switch s[i] {
case '\t', ' ', '(', ')', '<', '>', '[', ']':
break loop
}
}
if i != 0 {
tokenStr, remaining = s[:i], s[i:]
if tokenStr == "Not" {
return notTokenType, "", remaining
}
return strTokenType, tokenStr, remaining
}
j := 0
switch s[0] {
case '<':
j, tokenType = strings.IndexByte(s, '>'), angleTokenType
case '[':
j, tokenType = strings.IndexByte(s, ']'), squareTokenType
default:
return rune(s[0]), "", s[1:]
}
if j < 0 {
return errTokenType, "", ""
}
return tokenType, s[1:j], s[j+1:]
}
@@ -0,0 +1,699 @@
// Copyright 2018-2021 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 (
"context"
"encoding/xml"
"errors"
"fmt"
"io"
"net/http"
"path"
"strconv"
"strings"
"time"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/google/uuid"
ocdavErrors "github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/errors"
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/net"
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/prop"
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/spacelookup"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/opencloud-eu/reva/v2/pkg/utils"
"go.opentelemetry.io/otel/attribute"
)
// Most of this is taken from https://github.com/golang/net/blob/master/webdav/lock.go
// From RFC4918 http://www.webdav.org/specs/rfc4918.html#lock-tokens
// This specification encourages servers to create Universally Unique Identifiers (UUIDs) for lock tokens,
// and to use the URI form defined by "A Universally Unique Identifier (UUID) URN Namespace" ([RFC4122]).
// However, servers are free to use any URI (e.g., from another scheme) so long as it meets the uniqueness
// requirements. For example, a valid lock token might be constructed using the "opaquelocktoken" scheme
// defined in Appendix C.
//
// Example: "urn:uuid:f81d4fae-7dec-11d0-a765-00a0c91e6bf6"
//
// we stick to the recommendation and use the URN Namespace
const lockTokenPrefix = "urn:uuid:"
// TODO(jfd) implement lock
// see Web Distributed Authoring and Versioning (WebDAV) Locking Protocol:
// https://www.greenbytes.de/tech/webdav/draft-reschke-webdav-locking-latest.html
// Webdav supports a Depth: infinity lock, wopi only needs locks on files
// https://www.greenbytes.de/tech/webdav/draft-reschke-webdav-locking-latest.html#write.locks.and.the.if.request.header
// [...] a lock token MUST be submitted in the If header for all locked resources
// that a method may interact with or the method MUST fail. [...]
/*
COPY /~fielding/index.html HTTP/1.1
Host: example.com
Destination: http://example.com/users/f/fielding/index.html
If: <http://example.com/users/f/fielding/index.html>
(<opaquelocktoken:f81d4fae-7dec-11d0-a765-00a0c91e6bf6>)
*/
// http://www.webdav.org/specs/rfc4918.html#ELEMENT_lockinfo
type lockInfo struct {
XMLName xml.Name `xml:"lockinfo"`
Exclusive *struct{} `xml:"lockscope>exclusive"`
Shared *struct{} `xml:"lockscope>shared"`
Write *struct{} `xml:"locktype>write"`
Owner owner `xml:"owner"`
LockID string `xml:"locktoken>href"`
}
// http://www.webdav.org/specs/rfc4918.html#ELEMENT_owner
type owner struct {
InnerXML string `xml:",innerxml"`
}
// Condition can match a WebDAV resource, based on a token or ETag.
// Exactly one of Token and ETag should be non-empty.
type Condition struct {
Not bool
Token string
ETag string
}
// LockSystem manages access to a collection of named resources. The elements
// in a lock name are separated by slash ('/', U+002F) characters, regardless
// of host operating system convention.
type LockSystem interface {
// Confirm confirms that the caller can claim all of the locks specified by
// the given conditions, and that holding the union of all of those locks
// gives exclusive access to all of the named resources. Up to two resources
// can be named. Empty names are ignored.
//
// Exactly one of release and err will be non-nil. If release is non-nil,
// all of the requested locks are held until release is called. Calling
// release does not unlock the lock, in the WebDAV UNLOCK sense, but once
// Confirm has confirmed that a lock claim is valid, that lock cannot be
// Confirmed again until it has been released.
//
// If Confirm returns ErrConfirmationFailed then the Handler will continue
// to try any other set of locks presented (a WebDAV HTTP request can
// present more than one set of locks). If it returns any other non-nil
// error, the Handler will write a "500 Internal Server Error" HTTP status.
Confirm(ctx context.Context, now time.Time, name0, name1 string, conditions ...Condition) (release func(), err error)
// Create creates a lock with the given depth, duration, owner and root
// (name). The depth will either be negative (meaning infinite) or zero.
//
// If Create returns ErrLocked then the Handler will write a "423 Locked"
// HTTP status. If it returns any other non-nil error, the Handler will
// write a "500 Internal Server Error" HTTP status.
//
// See http://www.webdav.org/specs/rfc4918.html#rfc.section.9.10.6 for
// when to use each error.
//
// The token returned identifies the created lock. It should be an absolute
// URI as defined by RFC 3986, Section 4.3. In particular, it should not
// contain whitespace.
Create(ctx context.Context, now time.Time, details LockDetails) (token string, err error)
// Refresh refreshes the lock with the given token.
//
// If Refresh returns ErrLocked then the Handler will write a "423 Locked"
// HTTP Status. If Refresh returns ErrNoSuchLock then the Handler will write
// a "412 Precondition Failed" HTTP Status. If it returns any other non-nil
// error, the Handler will write a "500 Internal Server Error" HTTP status.
//
// See http://www.webdav.org/specs/rfc4918.html#rfc.section.9.10.6 for
// when to use each error.
Refresh(ctx context.Context, now time.Time, ref *provider.Reference, token string) error
// Unlock unlocks the lock with the given token.
//
// If Unlock returns ErrForbidden then the Handler will write a "403
// Forbidden" HTTP Status. If Unlock returns ErrLocked then the Handler
// will write a "423 Locked" HTTP status. If Unlock returns ErrNoSuchLock
// then the Handler will write a "409 Conflict" HTTP Status. If it returns
// any other non-nil error, the Handler will write a "500 Internal Server
// Error" HTTP status.
//
// See http://www.webdav.org/specs/rfc4918.html#rfc.section.9.11.1 for
// when to use each error.
Unlock(ctx context.Context, now time.Time, ref *provider.Reference, token string) error
}
// NewCS3LS returns a new CS3 based LockSystem.
func NewCS3LS(s pool.Selectable[gateway.GatewayAPIClient]) LockSystem {
return &cs3LS{
selector: s,
}
}
type cs3LS struct {
selector pool.Selectable[gateway.GatewayAPIClient]
}
func (cls *cs3LS) Confirm(ctx context.Context, now time.Time, name0, name1 string, conditions ...Condition) (func(), error) {
return nil, ocdavErrors.ErrNotImplemented
}
func (cls *cs3LS) Create(ctx context.Context, now time.Time, details LockDetails) (string, error) {
// always assume depth infinity?
/*
if !details.ZeroDepth {
The CS3 Lock api currently has no depth property, it only locks single resources
return "", ocdavErrors.ErrUnsupportedLockInfo
}
*/
u := ctxpkg.ContextMustGetUser(ctx)
// add metadata via opaque
// TODO: upate cs3api: https://github.com/cs3org/cs3apis/issues/213
o := utils.AppendPlainToOpaque(nil, "lockownername", u.GetDisplayName())
o = utils.AppendPlainToOpaque(o, "locktime", now.Format(time.RFC3339))
lockid := details.LockID
if lockid == "" {
// Having a lock token provides no special access rights. Anyone can find out anyone
// else's lock token by performing lock discovery. Locks must be enforced based upon
// whatever authentication mechanism is used by the server, not based on the secrecy
// of the token values.
// see: http://www.webdav.org/specs/rfc2518.html#n-lock-tokens
token := uuid.New()
lockid = lockTokenPrefix + token.String()
}
r := &provider.SetLockRequest{
Ref: details.Root,
Lock: &provider.Lock{
Opaque: o,
Type: provider.LockType_LOCK_TYPE_EXCL,
User: details.UserID, // no way to set an app lock? TODO maybe via the ownerxml
//AppName: , // TODO use a urn scheme?
LockId: lockid,
},
}
if details.Duration > 0 {
expiration := time.Now().UTC().Add(details.Duration)
r.Lock.Expiration = &types.Timestamp{
Seconds: uint64(expiration.Unix()),
Nanos: uint32(expiration.Nanosecond()),
}
}
client, err := cls.selector.Next()
if err != nil {
return "", err
}
res, err := client.SetLock(ctx, r)
if err != nil {
return "", err
}
switch res.GetStatus().GetCode() {
case rpc.Code_CODE_OK:
return lockid, nil
default:
return "", ocdavErrors.NewErrFromStatus(res.GetStatus())
}
}
func (cls *cs3LS) Refresh(ctx context.Context, now time.Time, ref *provider.Reference, token string) error {
u := ctxpkg.ContextMustGetUser(ctx)
// add metadata via opaque
// TODO: upate cs3api: https://github.com/cs3org/cs3apis/issues/213
o := utils.AppendPlainToOpaque(nil, "lockownername", u.GetDisplayName())
o = utils.AppendPlainToOpaque(o, "locktime", now.Format(time.RFC3339))
if token == "" {
return errors.New("token is empty")
}
r := &provider.RefreshLockRequest{
Ref: ref,
Lock: &provider.Lock{
Opaque: o,
Type: provider.LockType_LOCK_TYPE_EXCL,
//AppName: , // TODO use a urn scheme?
LockId: token,
User: u.GetId(),
},
}
client, err := cls.selector.Next()
if err != nil {
return err
}
res, err := client.RefreshLock(ctx, r)
if err != nil {
return err
}
switch res.GetStatus().GetCode() {
case rpc.Code_CODE_OK:
return nil
default:
return ocdavErrors.NewErrFromStatus(res.GetStatus())
}
}
func (cls *cs3LS) Unlock(ctx context.Context, now time.Time, ref *provider.Reference, token string) error {
u := ctxpkg.ContextMustGetUser(ctx)
r := &provider.UnlockRequest{
Ref: ref,
Lock: &provider.Lock{
LockId: token, // can be a token or a Coded-URL
User: u.Id,
},
}
client, err := cls.selector.Next()
if err != nil {
return err
}
res, err := client.Unlock(ctx, r)
if err != nil {
return err
}
newErr := ocdavErrors.NewErrFromStatus(res.GetStatus())
if newErr != nil {
appctx.GetLogger(ctx).Error().Str("token", token).Interface("unlock", ref).Msg("could not unlock " + res.GetStatus().GetMessage())
}
return newErr
}
// LockDetails are a lock's metadata.
type LockDetails struct {
// Root is the root resource name being locked. For a zero-depth lock, the
// root is the only resource being locked.
Root *provider.Reference
// Duration is the lock timeout. A negative duration means infinite.
Duration time.Duration
// OwnerXML is the verbatim <owner> XML given in a LOCK HTTP request.
//
// TODO: does the "verbatim" nature play well with XML namespaces?
// Does the OwnerXML field need to have more structure? See
// https://codereview.appspot.com/175140043/#msg2
OwnerXML string
UserID *userpb.UserId
// ZeroDepth is whether the lock has zero depth. If it does not have zero
// depth, it has infinite depth.
ZeroDepth bool
// OwnerName is the name of the lock owner
OwnerName string
// Locktime is the time the lock was created
Locktime time.Time
// LockID is the lock token
LockID string
}
func readLockInfo(r io.Reader) (li lockInfo, status int, err error) {
c := &countingReader{r: r}
if err = xml.NewDecoder(c).Decode(&li); err != nil {
if err == io.EOF {
if c.n == 0 {
// An empty body means to refresh the lock.
// http://www.webdav.org/specs/rfc4918.html#refreshing-locks
return lockInfo{}, 0, nil
}
err = ocdavErrors.ErrInvalidLockInfo
}
return lockInfo{}, http.StatusBadRequest, err
}
// We only support exclusive (non-shared) write locks. In practice, these are
// the only types of locks that seem to matter.
// We are ignoring the any properties in the lock details, and assume an exclusive write lock is requested.
// https://datatracker.ietf.org/doc/html/rfc4918#section-7 only describes write locks
//
// if li.Exclusive == nil || li.Shared != nil {
// return lockInfo{}, http.StatusNotImplemented, errors.ErrUnsupportedLockInfo
// }
// what should we return if the user requests a shared lock? or leaves out the locktype? the testsuite will only send the property lockscope, not locktype
// the oc tests cover both shared and exclusive locks. What is the WOPI lock? a shared or an exclusive lock?
// since it is issued by a service it seems to be an exclusive lock.
// the owner could be a link to the collaborative app ... to join the session
return li, 0, nil
}
type countingReader struct {
n int
r io.Reader
}
func (c *countingReader) Read(p []byte) (int, error) {
n, err := c.r.Read(p)
c.n += n
return n, err
}
const infiniteTimeout = -1
// parseTimeout parses the Timeout HTTP header, as per section 10.7. If s is
// empty, an infiniteTimeout is returned.
func parseTimeout(s string) (time.Duration, error) {
if s == "" {
return infiniteTimeout, nil
}
if i := strings.IndexByte(s, ','); i >= 0 {
s = s[:i]
}
s = strings.TrimSpace(s)
if s == "Infinite" {
return infiniteTimeout, nil
}
const pre = "Second-"
if !strings.HasPrefix(s, pre) {
return 0, ocdavErrors.ErrInvalidTimeout
}
s = s[len(pre):]
if s == "" || s[0] < '0' || '9' < s[0] {
return 0, ocdavErrors.ErrInvalidTimeout
}
n, err := strconv.ParseInt(s, 10, 64)
if err != nil || 1<<32-1 < n {
return 0, ocdavErrors.ErrInvalidTimeout
}
return time.Duration(n) * time.Second, nil
}
const (
infiniteDepth = -1
invalidDepth = -2
)
// parseDepth maps the strings "0", "1" and "infinity" to 0, 1 and
// infiniteDepth. Parsing any other string returns invalidDepth.
//
// Different WebDAV methods have further constraints on valid depths:
// - PROPFIND has no further restrictions, as per section 9.1.
// - COPY accepts only "0" or "infinity", as per section 9.8.3.
// - MOVE accepts only "infinity", as per section 9.9.2.
// - LOCK accepts only "0" or "infinity", as per section 9.10.3.
//
// These constraints are enforced by the handleXxx methods.
func parseDepth(s string) int {
switch s {
case "0":
return 0
case "1":
return 1
case "infinity":
return infiniteDepth
}
return invalidDepth
}
/*
the oc 10 wopi app code locks like this:
$storage->lockNodePersistent($file->getInternalPath(), [
'token' => $wopiLock,
'owner' => "{$user->getDisplayName()} via Office Online"
]);
if owner is empty it defaults to '{displayname} ({email})', which is not a url ... but ... shrug
The LockManager also defaults to exclusive locks:
$scope = ILock::LOCK_SCOPE_EXCLUSIVE;
if (isset($lockInfo['scope'])) {
$scope = $lockInfo['scope'];
}
*/
func (s *svc) handleLock(w http.ResponseWriter, r *http.Request, ns string) (retStatus int, retErr error) {
ctx, span := appctx.GetTracerProvider(r.Context()).Tracer(tracerName).Start(r.Context(), fmt.Sprintf("%s %v", r.Method, r.URL.Path))
defer span.End()
span.SetAttributes(attribute.String("component", "ocdav"))
fn := path.Join(ns, r.URL.Path) // TODO do we still need to jail if we query the registry about the spaces?
// TODO instead of using a string namespace ns pass in the space with the request?
ref, cs3Status, err := spacelookup.LookupReferenceForPath(ctx, s.gatewaySelector, fn)
if err != nil {
return http.StatusInternalServerError, err
}
if cs3Status.Code != rpc.Code_CODE_OK {
return http.StatusInternalServerError, ocdavErrors.NewErrFromStatus(cs3Status)
}
return s.lockReference(ctx, w, r, ref)
}
func (s *svc) handleSpacesLock(w http.ResponseWriter, r *http.Request, spaceID string) (retStatus int, retErr error) {
ctx, span := appctx.GetTracerProvider(r.Context()).Tracer(tracerName).Start(r.Context(), fmt.Sprintf("%s %v", r.Method, r.URL.Path))
defer span.End()
span.SetAttributes(attribute.String("component", "ocdav"))
ref, err := spacelookup.MakeStorageSpaceReference(spaceID, r.URL.Path)
if err != nil {
return http.StatusBadRequest, fmt.Errorf("invalid space id")
}
return s.lockReference(ctx, w, r, &ref)
}
func (s *svc) lockReference(ctx context.Context, w http.ResponseWriter, r *http.Request, ref *provider.Reference) (retStatus int, retErr error) {
sublog := appctx.GetLogger(ctx).With().Interface("ref", ref).Logger()
duration, err := parseTimeout(r.Header.Get(net.HeaderTimeout))
if err != nil {
return http.StatusBadRequest, ocdavErrors.ErrInvalidTimeout
}
li, status, err := readLockInfo(r.Body)
if err != nil {
return status, ocdavErrors.ErrInvalidLockInfo
}
u := ctxpkg.ContextMustGetUser(ctx)
token, now, created := "", time.Now(), false
ld := LockDetails{UserID: u.Id, Root: ref, Duration: duration, OwnerName: u.GetDisplayName(), Locktime: now, LockID: li.LockID}
if li == (lockInfo{}) {
// An empty lockInfo means to refresh the lock.
ih, ok := parseIfHeader(r.Header.Get(net.HeaderIf))
if !ok {
return http.StatusBadRequest, ocdavErrors.ErrInvalidIfHeader
}
if len(ih.lists) == 1 && len(ih.lists[0].conditions) == 1 {
token = ih.lists[0].conditions[0].Token
}
if token == "" {
return http.StatusBadRequest, ocdavErrors.ErrInvalidLockToken
}
err = s.LockSystem.Refresh(ctx, now, ref, token)
if err != nil {
if err == ocdavErrors.ErrNoSuchLock {
return http.StatusPreconditionFailed, err
}
return http.StatusInternalServerError, err
}
ld.LockID = token
} else {
// Section 9.10.3 says that "If no Depth header is submitted on a LOCK request,
// then the request MUST act as if a "Depth:infinity" had been submitted."
depth := infiniteDepth
if hdr := r.Header.Get(net.HeaderDepth); hdr != "" {
depth = parseDepth(hdr)
if depth != 0 && depth != infiniteDepth {
// Section 9.10.3 says that "Values other than 0 or infinity must not be
// used with the Depth header on a LOCK method".
return http.StatusBadRequest, ocdavErrors.ErrInvalidDepth
}
}
/* our url path has been shifted, so we don't need to do this?
reqPath, status, err := h.stripPrefix(r.URL.Path)
if err != nil {
return status, err
}
*/
// TODO look up username and email
// if li.Owner.InnerXML == "" {
// // PHP version: 'owner' => "{$user->getDisplayName()} via Office Online"
// ld.OwnerXML = ld.UserID.OpaqueId
// }
ld.OwnerXML = li.Owner.InnerXML // TODO optional, should be a URL
ld.ZeroDepth = depth == 0
//TODO: @jfd the code tries to create a lock for a file that may not even exist,
// should we do that in the decomposedfs as well? the node does not exist
// this actually is a name based lock ... ugh
token, err = s.LockSystem.Create(ctx, now, ld)
//
if err != nil {
switch {
case errors.Is(err, ocdavErrors.ErrLocked):
return http.StatusLocked, err
case errors.Is(err, ocdavErrors.ErrForbidden):
return http.StatusForbidden, err
default:
return http.StatusInternalServerError, err
}
}
defer func() {
if retErr != nil {
if err := s.LockSystem.Unlock(ctx, now, ref, token); err != nil {
appctx.GetLogger(ctx).Error().Err(err).Interface("lock", ld).Msg("could not unlock after failed lock")
}
}
}()
// Create the resource if it didn't previously exist.
// TODO use sdk to stat?
/*
if _, err := s.FileSystem.Stat(ctx, reqPath); err != nil {
f, err := s.FileSystem.OpenFile(ctx, reqPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
if err != nil {
// TODO: detect missing intermediate dirs and return http.StatusConflict?
return http.StatusInternalServerError, err
}
f.Close()
created = true
}
*/
// http://www.webdav.org/specs/rfc4918.html#HEADER_Lock-Token says that the
// Lock-Token value is a Coded-URL. We add angle brackets.
w.Header().Set("Lock-Token", "<"+token+">")
}
w.Header().Set("Content-Type", "application/xml; charset=utf-8")
if created {
// This is "w.WriteHeader(http.StatusCreated)" and not "return
// http.StatusCreated, nil" because we write our own (XML) response to w
// and Handler.ServeHTTP would otherwise write "Created".
w.WriteHeader(http.StatusCreated)
}
n, err := writeLockInfo(w, token, ld)
if err != nil {
sublog.Err(err).Int("bytes_written", n).Msg("error writing response")
}
return 0, nil
}
func writeLockInfo(w io.Writer, token string, ld LockDetails) (int, error) {
depth := "infinity"
if ld.ZeroDepth {
depth = "0"
}
href := ld.Root.Path // FIXME add base url and space?
lockdiscovery := strings.Builder{}
lockdiscovery.WriteString(xml.Header)
lockdiscovery.WriteString("<d:prop xmlns:d=\"DAV:\" xmlns:oc=\"http://owncloud.org/ns\"><d:lockdiscovery><d:activelock>\n")
lockdiscovery.WriteString(" <d:locktype><d:write/></d:locktype>\n")
lockdiscovery.WriteString(" <d:lockscope><d:exclusive/></d:lockscope>\n")
lockdiscovery.WriteString(fmt.Sprintf(" <d:depth>%s</d:depth>\n", depth))
if ld.OwnerXML != "" {
lockdiscovery.WriteString(fmt.Sprintf(" <d:owner>%s</d:owner>\n", ld.OwnerXML))
}
if ld.Duration > 0 {
timeout := ld.Duration / time.Second
lockdiscovery.WriteString(fmt.Sprintf(" <d:timeout>Second-%d</d:timeout>\n", timeout))
} else {
lockdiscovery.WriteString(" <d:timeout>Infinite</d:timeout>\n")
}
if token != "" {
lockdiscovery.WriteString(fmt.Sprintf(" <d:locktoken><d:href>%s</d:href></d:locktoken>\n", prop.Escape(token)))
}
if href != "" {
lockdiscovery.WriteString(fmt.Sprintf(" <d:lockroot><d:href>%s</d:href></d:lockroot>\n", prop.Escape(href)))
}
if ld.OwnerName != "" {
lockdiscovery.WriteString(fmt.Sprintf(" <oc:ownername>%s</oc:ownername>\n", prop.Escape(ld.OwnerName)))
}
if !ld.Locktime.IsZero() {
lockdiscovery.WriteString(fmt.Sprintf(" <oc:locktime>%s</oc:locktime>\n", prop.Escape(ld.Locktime.Format(time.RFC3339))))
}
lockdiscovery.WriteString("</d:activelock></d:lockdiscovery></d:prop>")
return fmt.Fprint(w, lockdiscovery.String())
}
func (s *svc) handleUnlock(w http.ResponseWriter, r *http.Request, ns string) (status int, err error) {
ctx, span := appctx.GetTracerProvider(r.Context()).Tracer(tracerName).Start(r.Context(), fmt.Sprintf("%s %v", r.Method, r.URL.Path))
defer span.End()
span.SetAttributes(attribute.String("component", "ocdav"))
fn := path.Join(ns, r.URL.Path) // TODO do we still need to jail if we query the registry about the spaces?
// TODO instead of using a string namespace ns pass in the space with the request?
ref, cs3Status, err := spacelookup.LookupReferenceForPath(ctx, s.gatewaySelector, fn)
if err != nil {
return http.StatusInternalServerError, err
}
if cs3Status.Code != rpc.Code_CODE_OK {
return http.StatusInternalServerError, ocdavErrors.NewErrFromStatus(cs3Status)
}
return s.unlockReference(ctx, w, r, ref)
}
func (s *svc) handleSpaceUnlock(w http.ResponseWriter, r *http.Request, spaceID string) (status int, err error) {
ctx, span := appctx.GetTracerProvider(r.Context()).Tracer(tracerName).Start(r.Context(), fmt.Sprintf("%s %v", r.Method, r.URL.Path))
defer span.End()
span.SetAttributes(attribute.String("component", "ocdav"))
ref, err := spacelookup.MakeStorageSpaceReference(spaceID, r.URL.Path)
if err != nil {
return http.StatusBadRequest, fmt.Errorf("invalid space id")
}
return s.unlockReference(ctx, w, r, &ref)
}
func (s *svc) unlockReference(ctx context.Context, _ http.ResponseWriter, r *http.Request, ref *provider.Reference) (retStatus int, retErr error) {
// http://www.webdav.org/specs/rfc4918.html#HEADER_Lock-Token says that the
// Lock-Token value should be a Coded-URL OR a token. We strip its angle brackets.
t := r.Header.Get(net.HeaderLockToken)
if len(t) > 2 && t[0] == '<' && t[len(t)-1] == '>' {
t = t[1 : len(t)-1]
}
err := s.LockSystem.Unlock(ctx, time.Now(), ref, t)
switch {
case err == nil:
return http.StatusNoContent, nil
case errors.Is(err, ocdavErrors.ErrLocked):
return http.StatusLocked, err
case errors.Is(err, ocdavErrors.ErrForbidden):
return http.StatusForbidden, err
}
return http.StatusInternalServerError, err
}
func requestLockToken(r *http.Request) string {
return strings.TrimSuffix(strings.TrimPrefix(r.Header.Get(net.HeaderLockToken), "<"), ">")
}
@@ -0,0 +1,245 @@
// Copyright 2018-2021 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 (
"encoding/xml"
"fmt"
"net/http"
"path"
"strings"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/config"
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/errors"
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/net"
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/prop"
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/propfind"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
"github.com/opencloud-eu/reva/v2/pkg/rhttp/router"
"github.com/opencloud-eu/reva/v2/pkg/storagespace"
)
// MetaHandler handles meta requests
type MetaHandler struct {
VersionsHandler *VersionsHandler
}
func (h *MetaHandler) init(c *config.Config) error {
h.VersionsHandler = new(VersionsHandler)
return h.VersionsHandler.init(c)
}
// Handler handles requests
func (h *MetaHandler) Handler(s *svc) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var id string
id, r.URL.Path = router.ShiftPath(r.URL.Path)
if id == "" {
if r.Method != MethodPropfind {
w.WriteHeader(http.StatusBadRequest)
return
}
h.handleEmptyID(w, r)
return
}
did, err := storagespace.ParseID(id)
if err != nil {
logger := appctx.GetLogger(r.Context())
logger.Debug().Str("prop", net.PropOcMetaPathForUser).Msg("invalid resource id")
w.WriteHeader(http.StatusBadRequest)
m := fmt.Sprintf("Invalid resource id %v", id)
b, err := errors.Marshal(http.StatusBadRequest, m, "", "")
errors.HandleWebdavError(logger, w, b, err)
return
}
if did.StorageId == "" && did.OpaqueId == "" && strings.Count(id, ":") >= 2 {
logger := appctx.GetLogger(r.Context())
logger.Warn().Str("id", id).Msg("detected invalid : separated resourceid id, trying to split it ... but fix the client that made the request")
// try splitting with :
parts := strings.SplitN(id, ":", 3)
did.StorageId = parts[0]
did.SpaceId = parts[1]
did.OpaqueId = parts[2]
}
var head string
head, r.URL.Path = router.ShiftPath(r.URL.Path)
switch head {
case "":
if r.Method != MethodPropfind {
w.WriteHeader(http.StatusBadRequest)
return
}
h.handlePathForUser(w, r, s, &did)
case "v":
h.VersionsHandler.Handler(s, &did).ServeHTTP(w, r)
default:
w.WriteHeader(http.StatusNotFound)
}
})
}
func (h *MetaHandler) handlePathForUser(w http.ResponseWriter, r *http.Request, s *svc, rid *provider.ResourceId) {
ctx, span := appctx.GetTracerProvider(r.Context()).Tracer(tracerName).Start(r.Context(), "meta_propfind")
defer span.End()
id := storagespace.FormatResourceID(rid)
sublog := appctx.GetLogger(ctx).With().Str("path", r.URL.Path).Str("resourceid", id).Logger()
sublog.Info().Msg("calling get path for user")
pf, status, err := propfind.ReadPropfind(r.Body)
if err != nil {
sublog.Debug().Err(err).Msg("error reading propfind request")
w.WriteHeader(status)
return
}
if ok := hasProp(&pf, net.PropOcMetaPathForUser); !ok {
sublog.Debug().Str("prop", net.PropOcMetaPathForUser).Msg("error finding prop in request")
w.WriteHeader(http.StatusBadRequest)
return
}
client, err := s.gatewaySelector.Next()
if err != nil {
sublog.Error().Err(err).Msg("error selecting next client")
w.WriteHeader(http.StatusInternalServerError)
return
}
pathReq := &provider.GetPathRequest{ResourceId: rid}
pathRes, err := client.GetPath(ctx, pathReq)
if err != nil {
sublog.Error().Err(err).Msg("could not send GetPath grpc request: transport error")
w.WriteHeader(http.StatusInternalServerError)
return
}
switch pathRes.Status.Code {
case rpc.Code_CODE_NOT_FOUND:
sublog.Debug().Str("code", string(pathRes.Status.Code)).Msg("resource not found")
w.WriteHeader(http.StatusNotFound)
m := fmt.Sprintf("Resource %s not found", id)
b, err := errors.Marshal(http.StatusNotFound, m, "", "")
errors.HandleWebdavError(&sublog, w, b, err)
return
case rpc.Code_CODE_PERMISSION_DENIED:
// raise StatusNotFound so that resources can't be enumerated
sublog.Debug().Str("code", string(pathRes.Status.Code)).Msg("resource access denied")
w.WriteHeader(http.StatusNotFound)
m := fmt.Sprintf("Resource %s not found", id)
b, err := errors.Marshal(http.StatusNotFound, m, "", "")
errors.HandleWebdavError(&sublog, w, b, err)
return
}
propstatOK := propfind.PropstatXML{
Status: "HTTP/1.1 200 OK",
Prop: []prop.PropertyXML{
prop.Escaped("oc:meta-path-for-user", pathRes.Path),
prop.Escaped("oc:id", id),
prop.Escaped("oc:fileid", id),
prop.Escaped("oc:spaceid", storagespace.FormatStorageID(rid.GetStorageId(), rid.GetSpaceId())),
},
}
baseURI := ctx.Value(net.CtxKeyBaseURI).(string)
msr := propfind.NewMultiStatusResponseXML()
msr.Responses = []*propfind.ResponseXML{
{
Href: net.EncodePath(path.Join(baseURI, id) + "/"),
Propstat: []propfind.PropstatXML{
propstatOK,
},
},
}
propRes, err := xml.Marshal(msr)
if err != nil {
sublog.Error().Err(err).Msg("error marshalling propfind response xml")
w.WriteHeader(http.StatusInternalServerError)
return
}
w.Header().Set(net.HeaderDav, "1, 3, extended-mkcol")
w.Header().Set(net.HeaderContentType, "application/xml; charset=utf-8")
w.WriteHeader(http.StatusMultiStatus)
if _, err := w.Write(propRes); err != nil {
sublog.Error().Err(err).Msg("error writing propfind response")
return
}
}
func (h *MetaHandler) handleEmptyID(w http.ResponseWriter, r *http.Request) {
ctx, span := appctx.GetTracerProvider(r.Context()).Tracer(tracerName).Start(r.Context(), "meta_propfind")
defer span.End()
sublog := appctx.GetLogger(ctx).With().Str("path", r.URL.Path).Logger()
pf, status, err := propfind.ReadPropfind(r.Body)
if err != nil {
sublog.Debug().Err(err).Msg("error reading propfind request")
w.WriteHeader(status)
return
}
if ok := hasProp(&pf, net.PropOcMetaPathForUser); !ok {
sublog.Debug().Str("prop", net.PropOcMetaPathForUser).Msg("error finding prop in request")
w.WriteHeader(http.StatusBadRequest)
return
}
propstatNotFound := propfind.PropstatXML{
Status: "HTTP/1.1 404 Not Found",
}
baseURI := ctx.Value(net.CtxKeyBaseURI).(string)
msr := propfind.NewMultiStatusResponseXML()
msr.Responses = []*propfind.ResponseXML{
{
Href: net.EncodePath(baseURI + "/"),
Propstat: []propfind.PropstatXML{
propstatNotFound,
},
},
}
propRes, err := xml.Marshal(msr)
if err != nil {
sublog.Error().Err(err).Msg("error marshalling propfind response xml")
w.WriteHeader(http.StatusInternalServerError)
return
}
w.Header().Set(net.HeaderDav, "1, 3, extended-mkcol")
w.Header().Set(net.HeaderContentType, "application/xml; charset=utf-8")
w.WriteHeader(http.StatusMultiStatus)
if _, err := w.Write(propRes); err != nil {
sublog.Error().Err(err).Msg("error writing propfind response")
return
}
}
func hasProp(pf *propfind.XML, key string) bool {
for i := range pf.Prop {
k := fmt.Sprintf("%s/%s", pf.Prop[i].Space, pf.Prop[i].Local)
if k == key {
return true
}
}
return false
}
@@ -0,0 +1,165 @@
// Copyright 2018-2021 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 (
"context"
"errors"
"fmt"
"net/http"
"path"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/spacelookup"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
rstatus "github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
"github.com/opencloud-eu/reva/v2/pkg/utils"
"github.com/rs/zerolog"
)
func (s *svc) handlePathMkcol(w http.ResponseWriter, r *http.Request, ns string) (status int, err error) {
ctx, span := appctx.GetTracerProvider(r.Context()).Tracer(tracerName).Start(r.Context(), "mkcol")
defer span.End()
if err := ValidateName(filename(r.URL.Path), s.nameValidators); err != nil {
return http.StatusBadRequest, err
}
fn := path.Join(ns, r.URL.Path)
sublog := appctx.GetLogger(ctx).With().Str("path", fn).Logger()
client, err := s.gatewaySelector.Next()
if err != nil {
return http.StatusInternalServerError, errtypes.InternalError(err.Error())
}
// stat requested path to make sure it isn't existing yet
// NOTE: It could be on another storage provider than the 'parent' of it
sr, err := client.Stat(ctx, &provider.StatRequest{
Ref: &provider.Reference{
Path: fn,
},
})
switch {
case err != nil:
return http.StatusInternalServerError, err
case sr.Status.Code == rpc.Code_CODE_OK:
// https://www.rfc-editor.org/rfc/rfc4918#section-9.3.1:
// 405 (Method Not Allowed) - MKCOL can only be executed on an unmapped URL.
return http.StatusMethodNotAllowed, fmt.Errorf("The resource you tried to create already exists")
case sr.Status.Code == rpc.Code_CODE_ABORTED:
return http.StatusPreconditionFailed, errtypes.NewErrtypeFromStatus(sr.Status)
case sr.Status.Code != rpc.Code_CODE_NOT_FOUND:
return rstatus.HTTPStatusFromCode(sr.Status.Code), errtypes.NewErrtypeFromStatus(sr.Status)
}
parentPath := path.Dir(fn)
space, rpcStatus, err := spacelookup.LookUpStorageSpaceForPath(ctx, s.gatewaySelector, parentPath)
switch {
case err != nil:
return http.StatusInternalServerError, err
case rpcStatus.Code == rpc.Code_CODE_NOT_FOUND:
// https://www.rfc-editor.org/rfc/rfc4918#section-9.3.1:
// 409 (Conflict) - A collection cannot be made at the Request-URI until
// one or more intermediate collections have been created. The server
// MUST NOT create those intermediate collections automatically.
return http.StatusConflict, fmt.Errorf("intermediate collection does not exist")
case rpcStatus.Code == rpc.Code_CODE_ABORTED:
return http.StatusPreconditionFailed, errtypes.NewErrtypeFromStatus(rpcStatus)
case rpcStatus.Code != rpc.Code_CODE_OK:
return rstatus.HTTPStatusFromCode(rpcStatus.Code), errtypes.NewErrtypeFromStatus(rpcStatus)
}
return s.handleMkcol(ctx, w, r, spacelookup.MakeRelativeReference(space, parentPath, false), spacelookup.MakeRelativeReference(space, fn, false), sublog)
}
func (s *svc) handleSpacesMkCol(w http.ResponseWriter, r *http.Request, spaceID string) (status int, err error) {
ctx, span := appctx.GetTracerProvider(r.Context()).Tracer(tracerName).Start(r.Context(), "spaces_mkcol")
defer span.End()
sublog := appctx.GetLogger(ctx).With().Str("path", r.URL.Path).Str("spaceid", spaceID).Str("handler", "mkcol").Logger()
parentRef, err := spacelookup.MakeStorageSpaceReference(spaceID, path.Dir(r.URL.Path))
if err != nil {
return http.StatusBadRequest, fmt.Errorf("invalid space id")
}
childRef, _ := spacelookup.MakeStorageSpaceReference(spaceID, r.URL.Path)
return s.handleMkcol(ctx, w, r, &parentRef, &childRef, sublog)
}
func (s *svc) handleMkcol(ctx context.Context, w http.ResponseWriter, r *http.Request, parentRef, childRef *provider.Reference, log zerolog.Logger) (status int, err error) {
if r.Body != http.NoBody {
// We currently do not support extended mkcol https://datatracker.ietf.org/doc/rfc5689/
// TODO let clients send a body with properties to set on the new resource
return http.StatusUnsupportedMediaType, fmt.Errorf("extended-mkcol not supported")
}
client, err := s.gatewaySelector.Next()
if err != nil {
return http.StatusInternalServerError, errtypes.InternalError(err.Error())
}
req := &provider.CreateContainerRequest{Ref: childRef}
res, err := client.CreateContainer(ctx, req)
switch {
case err != nil:
return http.StatusInternalServerError, err
case res.Status.Code == rpc.Code_CODE_OK:
w.WriteHeader(http.StatusCreated)
return 0, nil
case res.Status.Code == rpc.Code_CODE_NOT_FOUND:
// This should never happen because if the parent collection does not exist we should
// get a Code_CODE_FAILED_PRECONDITION. We play stupid and return what the response gave us
//lint:ignore ST1005 mimic the exact oc10 error message
return http.StatusNotFound, errors.New("Resource not found")
case res.Status.Code == rpc.Code_CODE_PERMISSION_DENIED:
// check if user has access to parent
sRes, err := client.Stat(ctx, &provider.StatRequest{Ref: &provider.Reference{
ResourceId: childRef.GetResourceId(),
Path: utils.MakeRelativePath(path.Dir(childRef.Path)),
}})
if err != nil {
return http.StatusInternalServerError, err
}
if sRes.Status.Code != rpc.Code_CODE_OK {
// return not found error so we do not leak existence of a file
// TODO hide permission failed for users without access in every kind of request
// TODO should this be done in the driver?
//lint:ignore ST1005 mimic the exact oc10 error message
return http.StatusNotFound, errors.New("Resource not found")
}
return http.StatusForbidden, errors.New(sRes.Status.Message)
case res.Status.Code == rpc.Code_CODE_ABORTED:
return http.StatusPreconditionFailed, errors.New(res.Status.Message)
case res.Status.Code == rpc.Code_CODE_FAILED_PRECONDITION:
// https://www.rfc-editor.org/rfc/rfc4918#section-9.3.1:
// 409 (Conflict) - A collection cannot be made at the Request-URI until
// one or more intermediate collections have been created. The server
// MUST NOT create those intermediate collections automatically.
return http.StatusConflict, errors.New(res.Status.Message)
case res.Status.Code == rpc.Code_CODE_ALREADY_EXISTS:
// https://www.rfc-editor.org/rfc/rfc4918#section-9.3.1:
// 405 (Method Not Allowed) - MKCOL can only be executed on an unmapped URL.
//lint:ignore ST1005 mimic the exact oc10 error message
return http.StatusMethodNotAllowed, errors.New("The resource you tried to create already exists")
}
return rstatus.HTTPStatusFromCode(res.Status.Code), errtypes.NewErrtypeFromStatus(res.Status)
}
@@ -0,0 +1,351 @@
// Copyright 2018-2021 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 (
"context"
"fmt"
"net/http"
"path"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/errors"
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/net"
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/spacelookup"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
rstatus "github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
"github.com/opencloud-eu/reva/v2/pkg/rhttp/router"
"github.com/opencloud-eu/reva/v2/pkg/storagespace"
"github.com/opencloud-eu/reva/v2/pkg/utils"
"github.com/rs/zerolog"
)
func (s *svc) handlePathMove(w http.ResponseWriter, r *http.Request, ns string) {
ctx, span := appctx.GetTracerProvider(r.Context()).Tracer(tracerName).Start(r.Context(), "move")
defer span.End()
if r.Body != http.NoBody {
w.WriteHeader(http.StatusUnsupportedMediaType)
b, err := errors.Marshal(http.StatusUnsupportedMediaType, "body must be empty", "", "")
errors.HandleWebdavError(appctx.GetLogger(ctx), w, b, err)
return
}
srcPath := path.Join(ns, r.URL.Path)
dh := r.Header.Get(net.HeaderDestination)
baseURI := r.Context().Value(net.CtxKeyBaseURI).(string)
dstPath, err := net.ParseDestination(baseURI, dh)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
b, err := errors.Marshal(http.StatusBadRequest, "failed to extract destination", "", "")
errors.HandleWebdavError(appctx.GetLogger(ctx), w, b, err)
return
}
if err := ValidateName(filename(srcPath), s.nameValidators); err != nil {
w.WriteHeader(http.StatusBadRequest)
b, err := errors.Marshal(http.StatusBadRequest, "source failed naming rules", "", "")
errors.HandleWebdavError(appctx.GetLogger(ctx), w, b, err)
return
}
if err := ValidateDestination(filename(dstPath), s.nameValidators); err != nil {
w.WriteHeader(http.StatusBadRequest)
b, err := errors.Marshal(http.StatusBadRequest, "destination naming rules", "", "")
errors.HandleWebdavError(appctx.GetLogger(ctx), w, b, err)
return
}
dstPath = path.Join(ns, dstPath)
sublog := appctx.GetLogger(ctx).With().Str("src", srcPath).Str("dst", dstPath).Logger()
srcSpace, status, err := spacelookup.LookUpStorageSpaceForPath(ctx, s.gatewaySelector, srcPath)
if err != nil {
sublog.Error().Err(err).Str("path", srcPath).Msg("failed to look up source storage space")
w.WriteHeader(http.StatusInternalServerError)
return
}
if status.Code != rpc.Code_CODE_OK {
errors.HandleErrorStatus(&sublog, w, status)
return
}
dstSpace, status, err := spacelookup.LookUpStorageSpaceForPath(ctx, s.gatewaySelector, dstPath)
if err != nil {
sublog.Error().Err(err).Str("path", dstPath).Msg("failed to look up destination storage space")
w.WriteHeader(http.StatusInternalServerError)
return
}
if status.Code != rpc.Code_CODE_OK {
errors.HandleErrorStatus(&sublog, w, status)
return
}
s.handleMove(ctx, w, r, spacelookup.MakeRelativeReference(srcSpace, srcPath, false), spacelookup.MakeRelativeReference(dstSpace, dstPath, false), sublog)
}
func (s *svc) handleSpacesMove(w http.ResponseWriter, r *http.Request, srcSpaceID string) {
ctx, span := appctx.GetTracerProvider(r.Context()).Tracer(tracerName).Start(r.Context(), "spaces_move")
defer span.End()
if r.Body != http.NoBody {
w.WriteHeader(http.StatusUnsupportedMediaType)
b, err := errors.Marshal(http.StatusUnsupportedMediaType, "body must be empty", "", "")
errors.HandleWebdavError(appctx.GetLogger(ctx), w, b, err)
return
}
dh := r.Header.Get(net.HeaderDestination)
baseURI := r.Context().Value(net.CtxKeyBaseURI).(string)
dst, err := net.ParseDestination(baseURI, dh)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
sublog := appctx.GetLogger(ctx).With().Str("spaceid", srcSpaceID).Str("path", r.URL.Path).Logger()
srcRef, err := spacelookup.MakeStorageSpaceReference(srcSpaceID, r.URL.Path)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
dstSpaceID, dstRelPath := router.ShiftPath(dst)
dstRef, err := spacelookup.MakeStorageSpaceReference(dstSpaceID, dstRelPath)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
s.handleMove(ctx, w, r, &srcRef, &dstRef, sublog)
}
func (s *svc) handleMove(ctx context.Context, w http.ResponseWriter, r *http.Request, src, dst *provider.Reference, log zerolog.Logger) {
isChild, err := s.referenceIsChildOf(ctx, s.gatewaySelector, dst, src)
if err != nil {
switch err.(type) {
case errtypes.IsNotFound:
w.WriteHeader(http.StatusNotFound)
case errtypes.IsNotSupported:
log.Error().Err(err).Msg("can not detect recursive move operation. missing machine auth configuration?")
w.WriteHeader(http.StatusForbidden)
default:
log.Error().Err(err).Msg("error while trying to detect recursive move operation")
w.WriteHeader(http.StatusInternalServerError)
}
return
}
if isChild {
w.WriteHeader(http.StatusConflict)
b, err := errors.Marshal(http.StatusBadRequest, "can not move a folder into one of its children", "", "")
errors.HandleWebdavError(&log, w, b, err)
return
}
isParent, err := s.referenceIsChildOf(ctx, s.gatewaySelector, src, dst)
if err != nil {
switch err.(type) {
case errtypes.IsNotFound:
isParent = false
case errtypes.IsNotSupported:
log.Error().Err(err).Msg("can not detect recursive move operation. missing machine auth configuration?")
w.WriteHeader(http.StatusForbidden)
return
default:
log.Error().Err(err).Msg("error while trying to detect recursive move operation")
w.WriteHeader(http.StatusInternalServerError)
return
}
}
if isParent {
w.WriteHeader(http.StatusConflict)
b, err := errors.Marshal(http.StatusBadRequest, "can not move a folder into its parent", "", "")
errors.HandleWebdavError(&log, w, b, err)
return
}
oh := r.Header.Get(net.HeaderOverwrite)
log.Debug().Str("overwrite", oh).Msg("move")
overwrite, err := net.ParseOverwrite(oh)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
client, err := s.gatewaySelector.Next()
if err != nil {
log.Error().Err(err).Msg("error selecting next client")
w.WriteHeader(http.StatusInternalServerError)
return
}
// check src exists
srcStatReq := &provider.StatRequest{Ref: src}
srcStatRes, err := client.Stat(ctx, srcStatReq)
if err != nil {
log.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 := errors.Marshal(http.StatusNotFound, m, "", "")
errors.HandleWebdavError(&log, w, b, err)
}
errors.HandleErrorStatus(&log, w, srcStatRes.Status)
return
}
if utils.IsSpaceRoot(srcStatRes.GetInfo()) {
log.Error().Msg("the source is disallowed")
w.WriteHeader(http.StatusBadRequest)
return
}
// check dst exists
dstStatReq := &provider.StatRequest{Ref: dst}
dstStatRes, err := client.Stat(ctx, dstStatReq)
if err != nil {
log.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 {
errors.HandleErrorStatus(&log, w, dstStatRes.Status)
return
}
successCode := http.StatusCreated // 201 if new resource was created, see https://tools.ietf.org/html/rfc4918#section-9.9.4
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.9.4
if utils.IsSpaceRoot(dstStatRes.GetInfo()) {
log.Error().Msg("overwriting is not allowed")
w.WriteHeader(http.StatusBadRequest)
return
}
if !overwrite {
log.Warn().Bool("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: dst}
delRes, err := client.Delete(ctx, delReq)
if err != nil {
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 {
errors.HandleErrorStatus(&log, w, delRes.Status)
return
}
} else {
// check if an intermediate path / the parent exists
intStatReq := &provider.StatRequest{Ref: &provider.Reference{
ResourceId: dst.ResourceId,
Path: utils.MakeRelativePath(path.Dir(dst.Path)),
}}
intStatRes, err := client.Stat(ctx, intStatReq)
if err != nil {
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
log.Debug().Interface("parent", dst).Interface("status", intStatRes.Status).Msg("conflict")
w.WriteHeader(http.StatusConflict)
} else {
errors.HandleErrorStatus(&log, w, intStatRes.Status)
}
return
}
// TODO what if intermediate is a file?
}
// resolve the destination path
if dst.Path == "." {
dst.Path = utils.MakeRelativePath(dstStatRes.GetInfo().GetName())
dst.ResourceId = dstStatRes.GetInfo().GetParentId()
}
mReq := &provider.MoveRequest{
Source: src,
Destination: dst,
LockId: requestLockToken(r),
}
mRes, err := client.Move(ctx, mReq)
if err != nil {
log.Error().Err(err).Msg("error sending move grpc request")
w.WriteHeader(http.StatusInternalServerError)
return
}
if mRes.Status.Code != rpc.Code_CODE_OK {
status := rstatus.HTTPStatusFromCode(mRes.Status.Code)
m := mRes.Status.Message
switch mRes.Status.Code {
case rpc.Code_CODE_ABORTED:
status = http.StatusPreconditionFailed
case rpc.Code_CODE_PERMISSION_DENIED:
status = http.StatusForbidden
case rpc.Code_CODE_UNIMPLEMENTED:
// We translate this into a Bad Gateway error as per https://www.rfc-editor.org/rfc/rfc4918#section-9.9.4
// > 502 (Bad Gateway) - This may occur when the destination is on another
// > server and the destination server refuses to accept the resource.
// > This could also occur when the destination is on another sub-section
// > of the same server namespace.
status = http.StatusBadGateway
}
w.WriteHeader(status)
b, err := errors.Marshal(status, m, "", "")
errors.HandleWebdavError(&log, w, b, err)
return
}
dstStatRes, err = client.Stat(ctx, dstStatReq)
if err != nil {
log.Error().Err(err).Msg("error sending grpc stat request")
w.WriteHeader(http.StatusInternalServerError)
return
}
if dstStatRes.Status.Code != rpc.Code_CODE_OK {
errors.HandleErrorStatus(&log, w, dstStatRes.Status)
return
}
info := dstStatRes.Info
w.Header().Set(net.HeaderContentType, info.MimeType)
w.Header().Set(net.HeaderETag, info.Etag)
w.Header().Set(net.HeaderOCFileID, storagespace.FormatResourceID(info.Id))
w.Header().Set(net.HeaderOCETag, info.Etag)
w.WriteHeader(successCode)
}
@@ -0,0 +1,38 @@
// Copyright 2018-2022 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 net
import (
"net/url"
"time"
cs3types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/opencloud-eu/reva/v2/pkg/utils"
)
// ContentDispositionAttachment builds a ContentDisposition Attachment header with various filename encodings
func ContentDispositionAttachment(filename string) string {
return "attachment; filename*=UTF-8''" + url.PathEscape(filename) + "; filename=\"" + filename + "\""
}
// RFC1123Z formats a CS3 Timestamp to be used in HTTP headers like Last-Modified
func RFC1123Z(ts *cs3types.Timestamp) string {
t := utils.TSToTime(ts).UTC()
return t.Format(time.RFC1123Z)
}
@@ -0,0 +1,43 @@
// Copyright 2018-2022 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 net
import (
"context"
userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
)
// IsCurrentUserOwnerOrManager returns whether the context user is the given owner or not
func IsCurrentUserOwnerOrManager(ctx context.Context, owner *userv1beta1.UserId, md *provider.ResourceInfo) bool {
contextUser, ok := ctxpkg.ContextGetUser(ctx)
// personal spaces have owners
if ok && contextUser.Id != nil && owner != nil &&
contextUser.Id.Idp == owner.Idp &&
contextUser.Id.OpaqueId == owner.OpaqueId {
return true
}
// check if the user is space manager
if md != nil && md.Owner != nil && md.Owner.GetType() == userv1beta1.UserType_USER_TYPE_SPACE_OWNER {
return md.GetPermissionSet().AddGrant
}
return false
}
@@ -0,0 +1,77 @@
// Copyright 2018-2022 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 net
// Common HTTP headers.
const (
HeaderAcceptRanges = "Accept-Ranges"
HeaderAccessControlAllowHeaders = "Access-Control-Allow-Headers"
HeaderAccessControlExposeHeaders = "Access-Control-Expose-Headers"
HeaderContentDisposistion = "Content-Disposition"
HeaderContentEncoding = "Content-Encoding"
HeaderContentLength = "Content-Length"
HeaderContentRange = "Content-Range"
HeaderContentType = "Content-Type"
HeaderETag = "ETag"
HeaderLastModified = "Last-Modified"
HeaderLocation = "Location"
HeaderRange = "Range"
HeaderIfMatch = "If-Match"
HeaderIfNoneMatch = "If-None-Match"
HeaderPrefer = "Prefer"
HeaderPreferenceApplied = "Preference-Applied"
HeaderVary = "Vary"
)
// webdav headers
const (
HeaderDav = "DAV" // https://datatracker.ietf.org/doc/html/rfc4918#section-10.1
HeaderDepth = "Depth" // https://datatracker.ietf.org/doc/html/rfc4918#section-10.2
HeaderDestination = "Destination" // https://datatracker.ietf.org/doc/html/rfc4918#section-10.3
HeaderIf = "If" // https://datatracker.ietf.org/doc/html/rfc4918#section-10.4
HeaderLockToken = "Lock-Token" // https://datatracker.ietf.org/doc/html/rfc4918#section-10.5
HeaderOverwrite = "Overwrite" // https://datatracker.ietf.org/doc/html/rfc4918#section-10.6
HeaderTimeout = "Timeout" // https://datatracker.ietf.org/doc/html/rfc4918#section-10.7
)
// Non standard HTTP headers.
const (
HeaderOCFileID = "OC-FileId"
HeaderOCETag = "OC-ETag"
HeaderOCChecksum = "OC-Checksum"
HeaderOCPermissions = "OC-Perm"
HeaderTusResumable = "Tus-Resumable"
HeaderTusVersion = "Tus-Version"
HeaderTusExtension = "Tus-Extension"
HeaderTusChecksumAlgorithm = "Tus-Checksum-Algorithm"
HeaderTusUploadExpires = "Upload-Expires"
HeaderUploadChecksum = "Upload-Checksum"
HeaderUploadLength = "Upload-Length"
HeaderUploadMetadata = "Upload-Metadata"
HeaderUploadOffset = "Upload-Offset"
HeaderOCMtime = "X-OC-Mtime"
HeaderExpectedEntityLength = "X-Expected-Entity-Length"
HeaderLitmus = "X-Litmus"
HeaderTransferAuth = "TransferHeaderAuthorization"
)
// HTTP Prefer header values
const (
HeaderPreferReturn = "return" // eg. return=representation / return=minimal, depth-noroot
)
@@ -0,0 +1,146 @@
// Copyright 2018-2022 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 net
import (
"net/url"
"strings"
"github.com/pkg/errors"
)
var (
// ErrInvalidHeaderValue defines an error which can occure when trying to parse a header value.
ErrInvalidHeaderValue = errors.New("invalid value")
)
type ctxKey int
const (
// CtxKeyBaseURI is the key of the base URI context field
CtxKeyBaseURI ctxKey = iota
// NsDav is the Dav ns
NsDav = "DAV:"
// NsOwncloud is the owncloud ns
NsOwncloud = "http://owncloud.org/ns"
// NsOCS is the OCS ns
NsOCS = "http://open-collaboration-services.org/ns"
// RFC1123 time that mimics oc10. time.RFC1123 would end in "UTC", see https://github.com/golang/go/issues/13781
RFC1123 = "Mon, 02 Jan 2006 15:04:05 GMT"
// PropQuotaUnknown is the quota unknown property
PropQuotaUnknown = "-2"
// PropOcFavorite is the favorite ns property
PropOcFavorite = "http://owncloud.org/ns/favorite"
// PropOcMetaPathForUser is the meta-path-for-user ns property
PropOcMetaPathForUser = "http://owncloud.org/ns/meta-path-for-user"
// DepthZero represents the webdav zero depth value
DepthZero Depth = "0"
// DepthOne represents the webdav one depth value
DepthOne Depth = "1"
// DepthInfinity represents the webdav infinity depth value
DepthInfinity Depth = "infinity"
)
// Depth is a type representing the webdav depth header value
type Depth string
// String returns the string representation of the webdav depth value
func (d Depth) String() string {
return string(d)
}
// EncodePath encodes the path of a url.
//
// slashes (/) are treated as path-separators.
func EncodePath(path string) string {
return (&url.URL{Path: path}).EscapedPath()
}
// ParseDepth parses the depth header value defined in https://tools.ietf.org/html/rfc4918#section-9.1
// Valid values are "0", "1" and "infinity". An empty string will be parsed to "1".
// For all other values this method returns an error.
func ParseDepth(s string) (Depth, error) {
if s == "" {
return DepthOne, nil
}
switch strings.ToLower(s) {
case DepthZero.String():
return DepthZero, nil
case DepthOne.String():
return DepthOne, nil
case DepthInfinity.String():
return DepthInfinity, nil
default:
return "", errors.Wrapf(ErrInvalidHeaderValue, "invalid depth: %s", s)
}
}
// ParseOverwrite parses the overwrite header value defined in https://datatracker.ietf.org/doc/html/rfc4918#section-10.6
// Valid values are "T" and "F". An empty string will be parse to true.
func ParseOverwrite(s string) (bool, error) {
if s == "" {
s = "T"
}
if s != "T" && s != "F" {
return false, errors.Wrapf(ErrInvalidHeaderValue, "invalid overwrite: %s", s)
}
return s == "T", nil
}
// ParseDestination parses the destination header value defined in https://datatracker.ietf.org/doc/html/rfc4918#section-10.3
// The returned path will be relative to the given baseURI.
func ParseDestination(baseURI, s string) (string, error) {
if s == "" {
return "", errors.Wrap(ErrInvalidHeaderValue, "destination header is empty")
}
dstURL, err := url.ParseRequestURI(s)
if err != nil {
return "", errors.Wrap(ErrInvalidHeaderValue, err.Error())
}
// TODO check if path is on same storage, return 502 on problems, see https://tools.ietf.org/html/rfc4918#section-9.9.4
// TODO make request.php optional in destination header
// 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.Wrap(ErrInvalidHeaderValue, "destination path does not contain base URI")
}
return urlSplit[1], nil
}
// ParsePrefer parses the prefer header value defined in https://datatracker.ietf.org/doc/html/rfc8144
func ParsePrefer(s string) map[string]string {
parts := strings.Split(s, ",")
m := make(map[string]string, len(parts))
for _, part := range parts {
kv := strings.SplitN(strings.ToLower(strings.Trim(part, " ")), "=", 2)
if len(kv) == 2 {
m[kv[0]] = kv[1]
} else {
m[kv[0]] = "1" // mark it as set
}
}
return m
}
@@ -0,0 +1,401 @@
// Copyright 2018-2021 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 (
"context"
"net/http"
"path"
"strings"
"time"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/jellydator/ttlcache/v2"
"github.com/mitchellh/mapstructure"
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/config"
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/net"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/opencloud-eu/reva/v2/pkg/rhttp"
"github.com/opencloud-eu/reva/v2/pkg/rhttp/global"
"github.com/opencloud-eu/reva/v2/pkg/rhttp/router"
"github.com/opencloud-eu/reva/v2/pkg/storage/favorite"
"github.com/opencloud-eu/reva/v2/pkg/storage/favorite/registry"
"github.com/opencloud-eu/reva/v2/pkg/storage/utils/templates"
"github.com/opencloud-eu/reva/v2/pkg/utils"
"github.com/rs/zerolog"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
)
// name is the Tracer name used to identify this instrumentation library.
const tracerName = "ocdav"
func init() {
global.Register("ocdav", New)
}
type svc struct {
c *config.Config
webDavHandler *WebDavHandler
davHandler *DavHandler
favoritesManager favorite.Manager
client *http.Client
gatewaySelector pool.Selectable[gateway.GatewayAPIClient]
// LockSystem is the lock management system.
LockSystem LockSystem
userIdentifierCache *ttlcache.Cache
nameValidators []Validator
}
func (s *svc) Config() *config.Config {
return s.c
}
func getFavoritesManager(c *config.Config) (favorite.Manager, error) {
if f, ok := registry.NewFuncs[c.FavoriteStorageDriver]; ok {
return f(c.FavoriteStorageDrivers[c.FavoriteStorageDriver])
}
return nil, errtypes.NotFound("driver not found: " + c.FavoriteStorageDriver)
}
func getLockSystem(c *config.Config) (LockSystem, error) {
// TODO in memory implementation
selector, err := pool.GatewaySelector(c.GatewaySvc)
if err != nil {
return nil, err
}
return NewCS3LS(selector), nil
}
// New returns a new ocdav service
func New(m map[string]interface{}, log *zerolog.Logger) (global.Service, error) {
conf := &config.Config{}
if err := mapstructure.Decode(m, conf); err != nil {
return nil, err
}
conf.Init()
fm, err := getFavoritesManager(conf)
if err != nil {
return nil, err
}
ls, err := getLockSystem(conf)
if err != nil {
return nil, err
}
return NewWith(conf, fm, ls, log, nil)
}
// NewWith returns a new ocdav service
func NewWith(conf *config.Config, fm favorite.Manager, ls LockSystem, _ *zerolog.Logger, selector pool.Selectable[gateway.GatewayAPIClient]) (global.Service, error) {
// be safe - init the conf again
conf.Init()
s := &svc{
c: conf,
webDavHandler: new(WebDavHandler),
davHandler: new(DavHandler),
client: rhttp.GetHTTPClient(
rhttp.Timeout(time.Duration(conf.Timeout*int64(time.Second))),
rhttp.Insecure(conf.Insecure),
),
gatewaySelector: selector,
favoritesManager: fm,
LockSystem: ls,
userIdentifierCache: ttlcache.NewCache(),
nameValidators: ValidatorsFromConfig(conf),
}
_ = s.userIdentifierCache.SetTTL(60 * time.Second)
// initialize handlers and set default configs
if err := s.webDavHandler.init(conf.WebdavNamespace, true); err != nil {
return nil, err
}
if err := s.davHandler.init(conf); err != nil {
return nil, err
}
if selector == nil {
var err error
s.gatewaySelector, err = pool.GatewaySelector(s.c.GatewaySvc)
if err != nil {
return nil, err
}
}
return s, nil
}
func (s *svc) Prefix() string {
return s.c.Prefix
}
func (s *svc) Close() error {
return nil
}
func (s *svc) Unprotected() []string {
return []string{"/status.php", "/status", "/remote.php/dav/public-files/", "/apps/files/", "/index.php/f/", "/index.php/s/", "/remote.php/dav/ocm/", "/dav/ocm/"}
}
func (s *svc) Handler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
log := appctx.GetLogger(ctx)
// TODO(jfd): do we need this?
// fake litmus testing for empty namespace: see https://github.com/golang/net/blob/e514e69ffb8bc3c76a71ae40de0118d794855992/webdav/litmus_test_server.go#L58-L89
if r.Header.Get(net.HeaderLitmus) == "props: 3 (propfind_invalid2)" {
http.Error(w, "400 Bad Request", http.StatusBadRequest)
return
}
// to build correct href prop urls we need to keep track of the base path
// always starts with /
base := path.Join("/", s.Prefix())
var head string
head, r.URL.Path = router.ShiftPath(r.URL.Path)
log.Debug().Str("method", r.Method).Str("head", head).Str("tail", r.URL.Path).Msg("http routing")
switch head {
case "status.php", "status":
s.doStatus(w, r)
return
case "remote.php":
// skip optional "remote.php"
head, r.URL.Path = router.ShiftPath(r.URL.Path)
// yet, add it to baseURI
base = path.Join(base, "remote.php")
case "apps":
head, r.URL.Path = router.ShiftPath(r.URL.Path)
if head == "files" {
s.handleLegacyPath(w, r)
return
}
case "index.php":
head, r.URL.Path = router.ShiftPath(r.URL.Path)
if head == "s" {
token := r.URL.Path
rURL := s.c.PublicURL + path.Join(head, token)
http.Redirect(w, r, rURL, http.StatusMovedPermanently)
return
}
}
switch head {
// the old `/webdav` endpoint uses remote.php/webdav/$path
case "webdav":
// for oc we need to prepend /home as the path that will be passed to the home storage provider
// will not contain the username
base = path.Join(base, "webdav")
ctx := context.WithValue(ctx, net.CtxKeyBaseURI, base)
r = r.WithContext(ctx)
s.webDavHandler.Handler(s).ServeHTTP(w, r)
return
case "dav":
// cern uses /dav/files/$namespace -> /$namespace/...
// oc uses /dav/files/$user -> /$home/$user/...
// for oc we need to prepend the path to user homes
// or we take the path starting at /dav and allow rewriting it?
base = path.Join(base, "dav")
ctx := context.WithValue(ctx, net.CtxKeyBaseURI, base)
r = r.WithContext(ctx)
s.davHandler.Handler(s).ServeHTTP(w, r)
return
}
log.Warn().Msg("resource not found")
w.WriteHeader(http.StatusNotFound)
})
}
func (s *svc) ApplyLayout(ctx context.Context, ns string, useLoggedInUserNS bool, requestPath string) (string, string, error) {
// If useLoggedInUserNS is false, that implies that the request is coming from
// the FilesHandler method invoked by a /dav/files/fileOwner where fileOwner
// is not the same as the logged in user. In that case, we'll treat fileOwner
// as the username whose files are to be accessed and use that in the
// namespace template.
u, ok := ctxpkg.ContextGetUser(ctx)
if !ok || !useLoggedInUserNS {
var requestUsernameOrID string
requestUsernameOrID, requestPath = router.ShiftPath(requestPath)
// Check if this is a Userid
client, err := s.gatewaySelector.Next()
if err != nil {
return "", "", err
}
userRes, err := client.GetUser(ctx, &userpb.GetUserRequest{
UserId: &userpb.UserId{OpaqueId: requestUsernameOrID},
})
if err != nil {
return "", "", err
}
// If it's not a userid try if it is a user name
if userRes.Status.Code != rpc.Code_CODE_OK {
res, err := client.GetUserByClaim(ctx, &userpb.GetUserByClaimRequest{
Claim: "username",
Value: requestUsernameOrID,
})
if err != nil {
return "", "", err
}
userRes.Status = res.Status
userRes.User = res.User
}
// If still didn't find a user, fallback
if userRes.Status.Code != rpc.Code_CODE_OK {
userRes.User = &userpb.User{
Username: requestUsernameOrID,
Id: &userpb.UserId{OpaqueId: requestUsernameOrID},
}
}
u = userRes.User
}
return templates.WithUser(u, ns), requestPath, nil
}
func authContextForUser(client gateway.GatewayAPIClient, userID *userpb.UserId, machineAuthAPIKey string) (context.Context, error) {
if machineAuthAPIKey == "" {
return nil, errtypes.NotSupported("machine auth not configured")
}
// Get auth
granteeCtx := ctxpkg.ContextSetUser(context.Background(), &userpb.User{Id: userID})
authRes, err := client.Authenticate(granteeCtx, &gateway.AuthenticateRequest{
Type: "machine",
ClientId: "userid:" + userID.OpaqueId,
ClientSecret: machineAuthAPIKey,
})
if err != nil {
return nil, err
}
if authRes.GetStatus().GetCode() != rpc.Code_CODE_OK {
return nil, errtypes.NewErrtypeFromStatus(authRes.Status)
}
granteeCtx = metadata.AppendToOutgoingContext(granteeCtx, ctxpkg.TokenHeader, authRes.Token)
return granteeCtx, nil
}
func (s *svc) sspReferenceIsChildOf(ctx context.Context, selector pool.Selectable[gateway.GatewayAPIClient], child, parent *provider.Reference) (bool, error) {
client, err := selector.Next()
if err != nil {
return false, err
}
parentStatRes, err := client.Stat(ctx, &provider.StatRequest{Ref: parent})
if err != nil {
return false, err
}
if parentStatRes.GetStatus().GetCode() != rpc.Code_CODE_OK {
return false, errtypes.NewErrtypeFromStatus(parentStatRes.GetStatus())
}
parentAuthCtx, err := authContextForUser(client, parentStatRes.GetInfo().GetOwner(), s.c.MachineAuthAPIKey)
if err != nil {
return false, err
}
parentPathRes, err := client.GetPath(parentAuthCtx, &provider.GetPathRequest{ResourceId: parentStatRes.GetInfo().GetId()})
if err != nil {
return false, err
}
childStatRes, err := client.Stat(ctx, &provider.StatRequest{Ref: child})
if err != nil {
return false, err
}
if childStatRes.GetStatus().GetCode() == rpc.Code_CODE_NOT_FOUND && utils.IsRelativeReference(child) && child.Path != "." {
childParentRef := &provider.Reference{
ResourceId: child.ResourceId,
Path: utils.MakeRelativePath(path.Dir(child.Path)),
}
childStatRes, err = client.Stat(ctx, &provider.StatRequest{Ref: childParentRef})
if err != nil {
return false, err
}
}
if childStatRes.GetStatus().GetCode() != rpc.Code_CODE_OK {
return false, errtypes.NewErrtypeFromStatus(parentStatRes.Status)
}
// TODO: this should use service accounts https://github.com/owncloud/ocis/issues/7597
childAuthCtx, err := authContextForUser(client, childStatRes.GetInfo().GetOwner(), s.c.MachineAuthAPIKey)
if err != nil {
return false, err
}
childPathRes, err := client.GetPath(childAuthCtx, &provider.GetPathRequest{ResourceId: childStatRes.GetInfo().GetId()})
if err != nil {
return false, err
}
cp := childPathRes.Path + "/"
pp := parentPathRes.Path + "/"
return strings.HasPrefix(cp, pp), nil
}
func (s *svc) referenceIsChildOf(ctx context.Context, selector pool.Selectable[gateway.GatewayAPIClient], child, parent *provider.Reference) (bool, error) {
if child.ResourceId.SpaceId != parent.ResourceId.SpaceId {
return false, nil // Not on the same storage -> not a child
}
if utils.ResourceIDEqual(child.ResourceId, parent.ResourceId) {
return strings.HasPrefix(child.Path, parent.Path+"/"), nil // Relative to the same resource -> compare paths
}
if child.ResourceId.SpaceId == utils.ShareStorageSpaceID || parent.ResourceId.SpaceId == utils.ShareStorageSpaceID {
// the sharesstorageprovider needs some special handling
return s.sspReferenceIsChildOf(ctx, selector, child, parent)
}
client, err := selector.Next()
if err != nil {
return false, err
}
// the references are on the same storage but relative to different resources
// -> we need to get the path for both resources
childPathRes, err := client.GetPath(ctx, &provider.GetPathRequest{ResourceId: child.ResourceId})
if err != nil {
if st, ok := status.FromError(err); ok && st.Code() == codes.Unimplemented {
return false, nil // the storage provider doesn't support GetPath() -> rely on it taking care of recursion issues
}
return false, err
}
parentPathRes, err := client.GetPath(ctx, &provider.GetPathRequest{ResourceId: parent.ResourceId})
if err != nil {
return false, err
}
cp := path.Join(childPathRes.Path, child.Path) + "/"
pp := path.Join(parentPathRes.Path, parent.Path) + "/"
return strings.HasPrefix(cp, pp), nil
}
// filename returns the base filename from a path and replaces any slashes with an empty string
func filename(p string) string {
return strings.Trim(path.Base(p), "/")
}
@@ -0,0 +1,48 @@
// Copyright 2018-2021 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"
"strings"
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/net"
)
func (s *svc) handleOptions(w http.ResponseWriter, r *http.Request) {
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(net.CtxKeyBaseURI).(string), "public-files")
w.Header().Set(net.HeaderContentType, "application/xml")
w.Header().Set("Allow", allow)
w.Header().Set("DAV", "1, 2")
w.Header().Set("MS-Author-Via", "DAV")
if !isPublic {
w.Header().Add(net.HeaderAccessControlAllowHeaders, net.HeaderTusResumable)
w.Header().Add(net.HeaderAccessControlExposeHeaders, strings.Join([]string{net.HeaderTusResumable, net.HeaderTusVersion, net.HeaderTusExtension}, ","))
w.Header().Set(net.HeaderTusResumable, "1.0.0") // TODO(jfd): only for dirs?
w.Header().Set(net.HeaderTusVersion, "1.0.0")
w.Header().Set(net.HeaderTusExtension, "creation,creation-with-upload,checksum,expiration")
w.Header().Set(net.HeaderTusChecksumAlgorithm, "md5,sha1,crc32")
}
w.WriteHeader(http.StatusNoContent)
}
@@ -0,0 +1,212 @@
// Copyright 2018-2021 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 prop
import (
"bytes"
"encoding/xml"
"unicode/utf8"
)
// PropertyXML represents a single DAV resource property as defined in RFC 4918.
// http://www.webdav.org/specs/rfc4918.html#data.model.for.resource.properties
type PropertyXML struct {
// XMLName is the fully qualified name that identifies this property.
XMLName xml.Name
// Lang is an optional xml:lang attribute.
Lang string `xml:"xml:lang,attr,omitempty"`
// InnerXML contains the XML representation of the property value.
// See http://www.webdav.org/specs/rfc4918.html#property_values
//
// Property values of complex type or mixed-content must have fully
// expanded XML namespaces or be self-contained with according
// XML namespace declarations. They must not rely on any XML
// namespace declarations within the scope of the XML document,
// even including the DAV: namespace.
InnerXML []byte `xml:",innerxml"`
}
func xmlEscaped(val string) []byte {
buf := new(bytes.Buffer)
xml.Escape(buf, []byte(val))
return buf.Bytes()
}
// EscapedNS returns a new PropertyXML instance while xml-escaping the value
func EscapedNS(namespace string, local string, val string) PropertyXML {
return PropertyXML{
XMLName: xml.Name{Space: namespace, Local: local},
Lang: "",
InnerXML: xmlEscaped(val),
}
}
var (
escAmp = []byte("&amp;")
escLT = []byte("&lt;")
escGT = []byte("&gt;")
escFFFD = []byte(string(utf8.RuneError)) // Unicode replacement character
)
// Decide whether the given rune is in the XML Character Range, per
// the Char production of https://www.xml.com/axml/testaxml.htm,
// Section 2.2 Characters.
func isInCharacterRange(r rune) (inrange bool) {
return r == 0x09 ||
r == 0x0A ||
r == 0x0D ||
r >= 0x20 && r <= 0xD7FF ||
r >= 0xE000 && r <= utf8.RuneError ||
r >= 0x10000 && r <= 0x10FFFF
}
// Escaped returns a new PropertyXML instance while replacing only
// * `&` with `&amp;`
// * `<` with `&lt;`
// * `>` with `&gt;`
// as defined in https://www.w3.org/TR/REC-xml/#syntax:
//
// > The ampersand character (&) and the left angle bracket (<) must not appear
// > in their literal form, except when used as markup delimiters, or within a
// > comment, a processing instruction, or a CDATA section. If they are needed
// > elsewhere, they must be escaped using either numeric character references
// > or the strings " &amp; " and " &lt; " respectively. The right angle
// > bracket (>) may be represented using the string " &gt; ", and must, for
// > compatibility, be escaped using either " &gt; " or a character reference
// > when it appears in the string " ]]> " in content, when that string is not
// > marking the end of a CDATA section.
//
// The code ignores errors as the legacy Escaped() does
// TODO properly use the space
func Escaped(key, val string) PropertyXML {
s := []byte(val)
w := bytes.NewBuffer(make([]byte, 0, len(s)))
var esc []byte
last := 0
for i := 0; i < len(s); {
r, width := utf8.DecodeRune(s[i:])
i += width
switch r {
case '&':
esc = escAmp
case '<':
esc = escLT
case '>':
esc = escGT
default:
if !isInCharacterRange(r) || (r == utf8.RuneError && width == 1) {
esc = escFFFD
break
}
continue
}
if _, err := w.Write(s[last : i-width]); err != nil {
break
}
if _, err := w.Write(esc); err != nil {
break
}
last = i
}
_, _ = w.Write(s[last:])
return PropertyXML{
XMLName: xml.Name{Space: "", Local: key},
Lang: "",
InnerXML: w.Bytes(),
}
}
// NotFound returns a new PropertyXML instance with an empty value
func NotFound(key string) PropertyXML {
return PropertyXML{
XMLName: xml.Name{Space: "", Local: key},
Lang: "",
}
}
// NotFoundNS returns a new PropertyXML instance with the given namespace and an empty value
func NotFoundNS(namespace, key string) PropertyXML {
return PropertyXML{
XMLName: xml.Name{Space: namespace, Local: key},
Lang: "",
}
}
// Raw returns a new PropertyXML instance for the given key/value pair
// TODO properly use the space
func Raw(key, val string) PropertyXML {
return PropertyXML{
XMLName: xml.Name{Space: "", Local: key},
Lang: "",
InnerXML: []byte(val),
}
}
// Next returns the next token, if any, in the XML stream of d.
// RFC 4918 requires to ignore comments, processing instructions
// and directives.
// http://www.webdav.org/specs/rfc4918.html#property_values
// http://www.webdav.org/specs/rfc4918.html#xml-extensibility
func Next(d *xml.Decoder) (xml.Token, error) {
for {
t, err := d.Token()
if err != nil {
return t, err
}
switch t.(type) {
case xml.Comment, xml.Directive, xml.ProcInst:
continue
default:
return t, nil
}
}
}
// ActiveLock holds active lock xml data
//
// http://www.webdav.org/specs/rfc4918.html#ELEMENT_activelock
//
// <!ELEMENT activelock (lockscope, locktype, depth, owner?, timeout?,
//
// locktoken?, lockroot)>
type ActiveLock struct {
XMLName xml.Name `xml:"activelock"`
Exclusive *struct{} `xml:"lockscope>exclusive,omitempty"`
Shared *struct{} `xml:"lockscope>shared,omitempty"`
Write *struct{} `xml:"locktype>write,omitempty"`
Depth string `xml:"depth"`
Owner Owner `xml:"owner,omitempty"`
Timeout string `xml:"timeout,omitempty"`
Locktoken string `xml:"locktoken>href"`
Lockroot string `xml:"lockroot>href,omitempty"`
}
// Owner captures the inner UML of a lock owner element http://www.webdav.org/specs/rfc4918.html#ELEMENT_owner
type Owner struct {
InnerXML string `xml:",innerxml"`
}
// Escape repaces ", &, ', < and > with their xml representation
func Escape(s string) string {
b := bytes.NewBuffer(nil)
_ = xml.EscapeText(b, []byte(s))
return b.String()
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,495 @@
// Copyright 2018-2021 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 (
"bytes"
"context"
"encoding/xml"
"fmt"
"io"
"net/http"
"path"
"strings"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/errors"
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/net"
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/prop"
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/propfind"
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/spacelookup"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/permission"
rstatus "github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
"github.com/opencloud-eu/reva/v2/pkg/utils"
"github.com/rs/zerolog"
)
func (s *svc) handlePathProppatch(w http.ResponseWriter, r *http.Request, ns string) (status int, err error) {
ctx, span := appctx.GetTracerProvider(r.Context()).Tracer(tracerName).Start(r.Context(), "proppatch")
defer span.End()
fn := path.Join(ns, r.URL.Path)
sublog := appctx.GetLogger(ctx).With().Str("path", fn).Logger()
pp, status, err := readProppatch(r.Body)
if err != nil {
return status, err
}
space, rpcStatus, err := spacelookup.LookUpStorageSpaceForPath(ctx, s.gatewaySelector, fn)
switch {
case err != nil:
return http.StatusInternalServerError, err
case rpcStatus.Code == rpc.Code_CODE_ABORTED:
return http.StatusPreconditionFailed, errtypes.NewErrtypeFromStatus(rpcStatus)
case rpcStatus.Code != rpc.Code_CODE_OK:
return rstatus.HTTPStatusFromCode(rpcStatus.Code), errtypes.NewErrtypeFromStatus(rpcStatus)
}
client, err := s.gatewaySelector.Next()
if err != nil {
return http.StatusInternalServerError, errtypes.InternalError(err.Error())
}
// check if resource exists
statReq := &provider.StatRequest{Ref: spacelookup.MakeRelativeReference(space, fn, false)}
statRes, err := client.Stat(ctx, statReq)
switch {
case err != nil:
return http.StatusInternalServerError, err
case statRes.Status.Code == rpc.Code_CODE_ABORTED:
return http.StatusPreconditionFailed, errtypes.NewErrtypeFromStatus(statRes.Status)
case statRes.Status.Code != rpc.Code_CODE_OK:
return rstatus.HTTPStatusFromCode(rpcStatus.Code), errtypes.NewErrtypeFromStatus(statRes.Status)
}
acceptedProps, removedProps, ok := s.handleProppatch(ctx, w, r, spacelookup.MakeRelativeReference(space, fn, false), pp, sublog)
if !ok {
// handleProppatch handles responses in error cases so return 0
return 0, nil
}
nRef := strings.TrimPrefix(fn, ns)
nRef = path.Join(ctx.Value(net.CtxKeyBaseURI).(string), nRef)
if statRes.Info.Type == provider.ResourceType_RESOURCE_TYPE_CONTAINER {
nRef += "/"
}
s.handleProppatchResponse(ctx, w, r, acceptedProps, removedProps, nRef, sublog)
return 0, nil
}
func (s *svc) handleSpacesProppatch(w http.ResponseWriter, r *http.Request, spaceID string) (status int, err error) {
ctx, span := appctx.GetTracerProvider(r.Context()).Tracer(tracerName).Start(r.Context(), "spaces_proppatch")
defer span.End()
sublog := appctx.GetLogger(ctx).With().Str("path", r.URL.Path).Str("spaceid", spaceID).Logger()
pp, status, err := readProppatch(r.Body)
if err != nil {
return status, err
}
ref, err := spacelookup.MakeStorageSpaceReference(spaceID, r.URL.Path)
if err != nil {
return http.StatusBadRequest, err
}
acceptedProps, removedProps, ok := s.handleProppatch(ctx, w, r, &ref, pp, sublog)
if !ok {
// handleProppatch handles responses in error cases so return 0
return 0, nil
}
nRef := path.Join(spaceID, r.URL.Path)
nRef = path.Join(ctx.Value(net.CtxKeyBaseURI).(string), nRef)
s.handleProppatchResponse(ctx, w, r, acceptedProps, removedProps, nRef, sublog)
return 0, nil
}
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) {
rreq := &provider.UnsetArbitraryMetadataRequest{
Ref: ref,
ArbitraryMetadataKeys: []string{""},
LockId: requestLockToken(r),
}
sreq := &provider.SetArbitraryMetadataRequest{
Ref: ref,
ArbitraryMetadata: &provider.ArbitraryMetadata{
Metadata: map[string]string{},
},
LockId: requestLockToken(r),
}
acceptedProps := []xml.Name{}
removedProps := []xml.Name{}
client, err := s.gatewaySelector.Next()
if err != nil {
log.Error().Err(err).Msg("error selecting next gateway client")
w.WriteHeader(http.StatusInternalServerError)
return nil, nil, false
}
for i := range patches {
if len(patches[i].Props) < 1 {
continue
}
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", 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"
value = s.as0or1(value)
if value == "0" {
remove = true
}
}
// Webdav spec requires the operations to be executed in the order
// specified in the PROPPATCH request
// http://www.webdav.org/specs/rfc2518.html#rfc.section.8.2
// FIXME: batch this somehow
if remove {
rreq.ArbitraryMetadataKeys[0] = key
res, err := client.UnsetArbitraryMetadata(ctx, rreq)
if err != nil {
log.Error().Err(err).Msg("error sending a grpc UnsetArbitraryMetadata request")
w.WriteHeader(http.StatusInternalServerError)
return nil, nil, false
}
if res.Status.Code != rpc.Code_CODE_OK {
status := rstatus.HTTPStatusFromCode(res.Status.Code)
if res.Status.Code == rpc.Code_CODE_ABORTED {
// aborted is used for etag an lock mismatches, which translates to 412
// in case a real Conflict response is needed, the calling code needs to send the header
status = http.StatusPreconditionFailed
}
m := res.Status.Message
if res.Status.Code == rpc.Code_CODE_PERMISSION_DENIED {
// check if user has access to resource
sRes, err := client.Stat(ctx, &provider.StatRequest{Ref: ref})
if err != nil {
log.Error().Err(err).Msg("error performing stat grpc request")
w.WriteHeader(http.StatusInternalServerError)
return nil, nil, false
}
if sRes.Status.Code != rpc.Code_CODE_OK {
// return not found error so we do not leak existence of a file
// TODO hide permission failed for users without access in every kind of request
// TODO should this be done in the driver?
status = http.StatusNotFound
}
}
if status == http.StatusNotFound {
m = "Resource not found" // mimic the oc10 error message
}
w.WriteHeader(status)
b, err := errors.Marshal(status, m, "", "")
errors.HandleWebdavError(&log, w, b, err)
return nil, nil, false
}
if key == "http://owncloud.org/ns/favorite" {
statRes, err := client.Stat(ctx, &provider.StatRequest{Ref: ref})
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return nil, nil, false
}
currentUser := ctxpkg.ContextMustGetUser(ctx)
ok, err := utils.CheckPermission(ctx, permission.WriteFavorites, client)
if err != nil {
log.Error().Err(err).Msg("error checking permission")
w.WriteHeader(http.StatusInternalServerError)
return nil, nil, false
}
if !ok {
log.Info().Interface("user", currentUser).Msg("user not allowed to unset favorite")
w.WriteHeader(http.StatusForbidden)
return nil, nil, false
}
err = s.favoritesManager.UnsetFavorite(ctx, currentUser.Id, statRes.Info)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return nil, nil, false
}
}
removedProps = append(removedProps, propNameXML)
} else {
sreq.ArbitraryMetadata.Metadata[key] = value
res, err := client.SetArbitraryMetadata(ctx, sreq)
if err != nil {
log.Error().Err(err).Str("key", key).Str("value", value).Msg("error sending a grpc SetArbitraryMetadata request")
w.WriteHeader(http.StatusInternalServerError)
return nil, nil, false
}
if res.Status.Code != rpc.Code_CODE_OK {
status := rstatus.HTTPStatusFromCode(res.Status.Code)
if res.Status.Code == rpc.Code_CODE_ABORTED {
// aborted is used for etag an lock mismatches, which translates to 412
// in case a real Conflict response is needed, the calling code needs to send the header
status = http.StatusPreconditionFailed
}
m := res.Status.Message
if res.Status.Code == rpc.Code_CODE_PERMISSION_DENIED {
// check if user has access to resource
sRes, err := client.Stat(ctx, &provider.StatRequest{Ref: ref})
if err != nil {
log.Error().Err(err).Msg("error performing stat grpc request")
w.WriteHeader(http.StatusInternalServerError)
return nil, nil, false
}
if sRes.Status.Code != rpc.Code_CODE_OK {
// return not found error so we don't leak existence of a file
// TODO hide permission failed for users without access in every kind of request
// TODO should this be done in the driver?
status = http.StatusNotFound
}
}
if status == http.StatusNotFound {
m = "Resource not found" // mimic the oc10 error message
}
w.WriteHeader(status)
b, err := errors.Marshal(status, m, "", "")
errors.HandleWebdavError(&log, w, b, err)
return nil, nil, false
}
acceptedProps = append(acceptedProps, propNameXML)
delete(sreq.ArbitraryMetadata.Metadata, key)
if key == "http://owncloud.org/ns/favorite" {
statRes, err := client.Stat(ctx, &provider.StatRequest{Ref: ref})
if err != nil || statRes.Info == nil {
w.WriteHeader(http.StatusInternalServerError)
return nil, nil, false
}
currentUser := ctxpkg.ContextMustGetUser(ctx)
ok, err := utils.CheckPermission(ctx, permission.WriteFavorites, client)
if err != nil {
log.Error().Err(err).Msg("error checking permission")
w.WriteHeader(http.StatusInternalServerError)
return nil, nil, false
}
if !ok {
log.Info().Interface("user", currentUser).Msg("user not allowed to set favorite")
w.WriteHeader(http.StatusForbidden)
return nil, nil, false
}
err = s.favoritesManager.SetFavorite(ctx, currentUser.Id, statRes.Info)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return nil, nil, false
}
}
}
}
// FIXME: in case of error, need to set all properties back to the original state,
// and return the error in the matching propstat block, if applicable
// http://www.webdav.org/specs/rfc2518.html#rfc.section.8.2
}
return acceptedProps, removedProps, true
}
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 {
log.Error().Err(err).Msg("error formatting proppatch response")
w.WriteHeader(http.StatusInternalServerError)
return
}
w.Header().Set(net.HeaderDav, "1, 3, extended-mkcol")
w.Header().Set(net.HeaderContentType, "application/xml; charset=utf-8")
w.WriteHeader(http.StatusMultiStatus)
if _, err := w.Write(propRes); err != nil {
log.Err(err).Msg("error writing response")
}
}
func (s *svc) formatProppatchResponse(ctx context.Context, acceptedProps []xml.Name, removedProps []xml.Name, ref string) ([]byte, error) {
responses := make([]propfind.ResponseXML, 0, 1)
response := propfind.ResponseXML{
Href: net.EncodePath(ref),
Propstat: []propfind.PropstatXML{},
}
if len(acceptedProps) > 0 {
propstatBody := []prop.PropertyXML{}
for i := range acceptedProps {
propstatBody = append(propstatBody, prop.EscapedNS(acceptedProps[i].Space, acceptedProps[i].Local, ""))
}
response.Propstat = append(response.Propstat, propfind.PropstatXML{
Status: "HTTP/1.1 200 OK",
Prop: propstatBody,
})
}
if len(removedProps) > 0 {
propstatBody := []prop.PropertyXML{}
for i := range removedProps {
propstatBody = append(propstatBody, prop.EscapedNS(removedProps[i].Space, removedProps[i].Local, ""))
}
response.Propstat = append(response.Propstat, propfind.PropstatXML{
Status: "HTTP/1.1 204 No Content",
Prop: propstatBody,
})
}
responses = append(responses, response)
responsesXML, err := xml.Marshal(&responses)
if err != nil {
return nil, err
}
var buf bytes.Buffer
buf.WriteString(`<?xml version="1.0" encoding="utf-8"?><d:multistatus xmlns:d="DAV:" `)
buf.WriteString(`xmlns:s="http://sabredav.org/ns" xmlns:oc="http://owncloud.org/ns">`)
buf.Write(responsesXML)
buf.WriteString(`</d:multistatus>`)
return buf.Bytes(), nil
}
func (s *svc) isBooleanProperty(prop string) bool {
// TODO add other properties we know to be boolean?
return prop == net.PropOcFavorite
}
func (s *svc) as0or1(val string) string {
switch strings.TrimSpace(val) {
case "false":
return "0"
case "":
return "0"
case "0":
return "0"
case "no":
return "0"
case "off":
return "0"
}
return "1"
}
// Proppatch describes a property update instruction as defined in RFC 4918.
// See http://www.webdav.org/specs/rfc4918.html#METHOD_PROPPATCH
type Proppatch struct {
// Remove specifies whether this patch removes properties. If it does not
// remove them, it sets them.
Remove bool
// Props contains the properties to be set or removed.
Props []prop.PropertyXML
}
// http://www.webdav.org/specs/rfc4918.html#ELEMENT_prop (for proppatch)
type proppatchProps []prop.PropertyXML
// UnmarshalXML appends the property names and values enclosed within start
// to ps.
//
// An xml:lang attribute that is defined either on the DAV:prop or property
// name XML element is propagated to the property's Lang field.
//
// UnmarshalXML returns an error if start does not contain any properties or if
// property values contain syntactically incorrect XML.
func (ps *proppatchProps) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
lang := xmlLang(start, "")
for {
t, err := prop.Next(d)
if err != nil {
return err
}
switch elem := t.(type) {
case xml.EndElement:
if len(*ps) == 0 {
return fmt.Errorf("%s must not be empty", start.Name.Local)
}
return nil
case xml.StartElement:
p := prop.PropertyXML{}
err = d.DecodeElement(&p, &elem)
if err != nil {
return err
}
// special handling for the lang property
p.Lang = xmlLang(t.(xml.StartElement), lang)
*ps = append(*ps, p)
}
}
}
// http://www.webdav.org/specs/rfc4918.html#ELEMENT_set
// http://www.webdav.org/specs/rfc4918.html#ELEMENT_remove
type setRemove struct {
XMLName xml.Name
Lang string `xml:"xml:lang,attr,omitempty"`
Prop proppatchProps `xml:"DAV: prop"`
}
// http://www.webdav.org/specs/rfc4918.html#ELEMENT_propertyupdate
type propertyupdate struct {
XMLName xml.Name `xml:"DAV: propertyupdate"`
Lang string `xml:"xml:lang,attr,omitempty"`
SetRemove []setRemove `xml:",any"`
}
func readProppatch(r io.Reader) (patches []Proppatch, status int, err error) {
var pu propertyupdate
if err = xml.NewDecoder(r).Decode(&pu); err != nil {
return nil, http.StatusBadRequest, err
}
for _, op := range pu.SetRemove {
remove := false
switch op.XMLName {
case xml.Name{Space: net.NsDav, Local: "set"}:
// No-op.
case xml.Name{Space: net.NsDav, Local: "remove"}:
for _, p := range op.Prop {
if len(p.InnerXML) > 0 {
return nil, http.StatusBadRequest, errors.ErrInvalidProppatch
}
}
remove = true
default:
return nil, http.StatusBadRequest, errors.ErrInvalidProppatch
}
patches = append(patches, Proppatch{Remove: remove, Props: op.Prop})
}
return patches, 0, nil
}
var xmlLangName = xml.Name{Space: "http://www.w3.org/XML/1998/namespace", Local: "lang"}
func xmlLang(s xml.StartElement, d string) string {
for _, attr := range s.Attr {
if attr.Name == xmlLangName {
return attr.Value
}
}
return d
}
@@ -0,0 +1,201 @@
// Copyright 2018-2021 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 (
"context"
"fmt"
"net/http"
"path"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
typesv1beta1 "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
ocdaverrors "github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/errors"
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/net"
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/propfind"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
"github.com/opencloud-eu/reva/v2/pkg/rhttp/router"
"go.opentelemetry.io/otel/codes"
semconv "go.opentelemetry.io/otel/semconv/v1.20.0"
)
// PublicFileHandler handles requests on a shared file. it needs to be wrapped in a collection
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) {
ctx := r.Context()
log := appctx.GetLogger(ctx)
token, relativePath := router.ShiftPath(r.URL.Path)
base := path.Join(ctx.Value(net.CtxKeyBaseURI).(string), token)
ctx = context.WithValue(ctx, net.CtxKeyBaseURI, base)
r = r.WithContext(ctx)
log.Debug().Str("relativePath", relativePath).Msg("PublicFileHandler func")
if relativePath != "" && relativePath != "/" {
// accessing the file
switch r.Method {
case MethodPropfind:
s.handlePropfindOnToken(w, r, h.namespace, false)
case http.MethodGet:
s.handlePathGet(w, r, h.namespace)
case http.MethodOptions:
s.handleOptions(w, r)
case http.MethodHead:
s.handlePathHead(w, r, h.namespace)
case http.MethodPut:
s.handlePathPut(w, r, h.namespace)
default:
w.WriteHeader(http.StatusMethodNotAllowed)
}
} else {
// accessing the virtual parent folder
switch r.Method {
case MethodPropfind:
s.handlePropfindOnToken(w, r, h.namespace, true)
case http.MethodOptions:
s.handleOptions(w, r)
case http.MethodHead:
s.handlePathHead(w, r, h.namespace)
default:
w.WriteHeader(http.StatusMethodNotAllowed)
}
}
})
}
// 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, span := appctx.GetTracerProvider(r.Context()).Tracer(tracerName).Start(r.Context(), "token_propfind")
defer span.End()
tokenStatInfo, ok := TokenStatInfoFromContext(ctx)
if !ok {
span.RecordError(ocdaverrors.ErrTokenStatInfoMissing)
span.SetStatus(codes.Error, ocdaverrors.ErrTokenStatInfoMissing.Error())
span.SetAttributes(semconv.HTTPStatusCodeKey.Int(http.StatusInternalServerError))
w.WriteHeader(http.StatusInternalServerError)
b, err := ocdaverrors.Marshal(http.StatusInternalServerError, ocdaverrors.ErrTokenStatInfoMissing.Error(), "", "")
ocdaverrors.HandleWebdavError(appctx.GetLogger(ctx), w, b, err)
return
}
sublog := appctx.GetLogger(ctx).With().Interface("tokenStatInfo", tokenStatInfo).Logger()
sublog.Debug().Msg("handlePropfindOnToken")
dh := r.Header.Get(net.HeaderDepth)
depth, err := net.ParseDepth(dh)
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, "Invalid Depth header value")
span.SetAttributes(semconv.HTTPStatusCodeKey.Int(http.StatusBadRequest))
sublog.Debug().Str("depth", dh).Msg(err.Error())
w.WriteHeader(http.StatusBadRequest)
m := fmt.Sprintf("Invalid Depth header value: %v", dh)
b, err := ocdaverrors.Marshal(http.StatusBadRequest, m, "", "")
ocdaverrors.HandleWebdavError(&sublog, w, b, err)
return
}
if depth == net.DepthInfinity && !s.c.AllowPropfindDepthInfinitiy {
span.RecordError(ocdaverrors.ErrInvalidDepth)
span.SetStatus(codes.Error, "DEPTH: infinity is not supported")
span.SetAttributes(semconv.HTTPStatusCodeKey.Int(http.StatusBadRequest))
sublog.Debug().Str("depth", dh).Msg(ocdaverrors.ErrInvalidDepth.Error())
w.WriteHeader(http.StatusBadRequest)
m := fmt.Sprintf("Invalid Depth header value: %v", dh)
b, err := ocdaverrors.Marshal(http.StatusBadRequest, m, "", "")
ocdaverrors.HandleWebdavError(&sublog, w, b, err)
return
}
pf, status, err := propfind.ReadPropfind(r.Body)
if err != nil {
sublog.Debug().Err(err).Msg("error reading propfind request")
w.WriteHeader(status)
return
}
infos := s.getPublicFileInfos(onContainer, depth == net.DepthZero, tokenStatInfo)
prefer := net.ParsePrefer(r.Header.Get("prefer"))
returnMinimal := prefer[net.HeaderPreferReturn] == "minimal"
propRes, err := propfind.MultistatusResponse(ctx, &pf, infos, s.c.PublicURL, ns, nil, returnMinimal)
if err != nil {
sublog.Error().Err(err).Msg("error formatting propfind")
w.WriteHeader(http.StatusInternalServerError)
return
}
w.Header().Set(net.HeaderDav, "1, 3, extended-mkcol")
w.Header().Set(net.HeaderContentType, "application/xml; charset=utf-8")
w.Header().Set(net.HeaderVary, net.HeaderPrefer)
if returnMinimal {
w.Header().Set(net.HeaderPreferenceApplied, "return=minimal")
}
w.WriteHeader(http.StatusMultiStatus)
if _, err := w.Write(propRes); err != nil {
sublog.Err(err).Msg("error writing response")
}
}
// there are only two possible entries
// 1. the non existing collection
// 2. the shared file
func (s *svc) getPublicFileInfos(onContainer, onlyRoot bool, i *provider.ResourceInfo) []*provider.ResourceInfo {
infos := []*provider.ResourceInfo{}
if onContainer {
// copy link-share data if present
// we don't copy everything because the checksum should not be present
var o *typesv1beta1.Opaque
if i.Opaque != nil && i.Opaque.Map != nil && i.Opaque.Map["link-share"] != nil {
o = &typesv1beta1.Opaque{
Map: map[string]*typesv1beta1.OpaqueEntry{
"link-share": i.Opaque.Map["link-share"],
},
}
}
// always add collection
infos = append(infos, &provider.ResourceInfo{
// Opaque carries the link-share data we need when rendering the collection root href
Opaque: o,
Path: path.Dir(i.Path),
Type: provider.ResourceType_RESOURCE_TYPE_CONTAINER,
})
if onlyRoot {
return infos
}
}
// add the file info
infos = append(infos, i)
return infos
}
@@ -0,0 +1,456 @@
// Copyright 2018-2021 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 (
"context"
"io"
"net/http"
"path"
"path/filepath"
"strconv"
"strings"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
typespb "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/opencloud-eu/reva/v2/internal/http/services/datagateway"
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/errors"
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/net"
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/spacelookup"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/rhttp"
"github.com/opencloud-eu/reva/v2/pkg/storagespace"
"github.com/opencloud-eu/reva/v2/pkg/utils"
"github.com/rs/zerolog"
"go.opentelemetry.io/otel/propagation"
)
func sufferMacOSFinder(r *http.Request) bool {
return r.Header.Get(net.HeaderExpectedEntityLength) != ""
}
func handleMacOSFinder(w http.ResponseWriter, r *http.Request) error {
/*
Many webservers will not cooperate well with Finder PUT requests,
because it uses 'Chunked' transfer encoding for the request body.
The symptom of this problem is that Finder sends files to the
server, but they arrive as 0-length files.
If we don't do anything, the user might think they are uploading
files successfully, but they end up empty on the server. Instead,
we throw back an error if we detect this.
The reason Finder uses Chunked, is because it thinks the files
might change as it's being uploaded, and therefore the
Content-Length can vary.
Instead it sends the X-Expected-Entity-Length header with the size
of the file at the very start of the request. If this header is set,
but we don't get a request body we will fail the request to
protect the end-user.
*/
log := appctx.GetLogger(r.Context())
content := r.Header.Get(net.HeaderContentLength)
expected := r.Header.Get(net.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.
// Another possible mitigation is to change the use the value of X-Expected-Entity-Length header in the Content-Length header.
expectedInt, err := strconv.ParseInt(expected, 10, 64)
if err != nil {
log.Error().Err(err).Msg("error parsing expected length")
w.WriteHeader(http.StatusBadRequest)
return err
}
r.ContentLength = expectedInt
return nil
}
func isContentRange(r *http.Request) bool {
/*
Content-Range is dangerous for PUT requests: PUT per definition
stores a full resource. draft-ietf-httpbis-p2-semantics-15 says
in section 7.6:
An origin server SHOULD reject any PUT request that contains a
Content-Range header field, since it might be misinterpreted as
partial content (or might be partial content that is being mistakenly
PUT as a full representation). Partial content updates are possible
by targeting a separately identified resource with state that
overlaps a portion of the larger resource, or by using a different
method that has been specifically defined for partial updates (for
example, the PATCH method defined in [RFC5789]).
This clarifies RFC2616 section 9.6:
The recipient of the entity MUST NOT ignore any Content-*
(e.g. Content-Range) headers that it does not understand or implement
and MUST return a 501 (Not Implemented) response in such cases.
OTOH is a PUT request with a Content-Range currently the only way to
continue an aborted upload request and is supported by curl, mod_dav,
Tomcat and others. Since some clients do use this feature which results
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(net.HeaderContentRange) != ""
}
func (s *svc) handlePathPut(w http.ResponseWriter, r *http.Request, ns string) {
ctx, span := appctx.GetTracerProvider(r.Context()).Tracer(tracerName).Start(r.Context(), "put")
defer span.End()
fn := path.Join(ns, r.URL.Path)
sublog := appctx.GetLogger(ctx).With().Str("path", fn).Logger()
if err := ValidateName(filename(r.URL.Path), s.nameValidators); err != nil {
w.WriteHeader(http.StatusBadRequest)
b, err := errors.Marshal(http.StatusBadRequest, err.Error(), "", "")
errors.HandleWebdavError(&sublog, w, b, err)
return
}
space, status, err := spacelookup.LookUpStorageSpaceForPath(ctx, s.gatewaySelector, fn)
if err != nil {
sublog.Error().Err(err).Str("path", fn).Msg("failed to look up storage space")
w.WriteHeader(http.StatusInternalServerError)
return
}
if status.Code != rpc.Code_CODE_OK {
errors.HandleErrorStatus(&sublog, w, status)
return
}
s.handlePut(ctx, w, r, spacelookup.MakeRelativeReference(space, fn, false), sublog)
}
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 := getContentLength(r)
if err != nil {
log.Error().Err(err).Msg("error getting the content length")
w.WriteHeader(http.StatusBadRequest)
return
}
client, err := s.gatewaySelector.Next()
if err != nil {
log.Error().Err(err).Msg("error selecting next gateway client")
w.WriteHeader(http.StatusInternalServerError)
return
}
// Test if the target is a secret filedrop
tokenStatInfo, ok := TokenStatInfoFromContext(ctx)
// We assume that when the uploader can create containers, but is not allowed to list them, it is a secret file drop
if ok && tokenStatInfo.GetPermissionSet().CreateContainer && !tokenStatInfo.GetPermissionSet().ListContainer {
// TODO we can skip this stat if the tokenStatInfo is the direct parent
sReq := &provider.StatRequest{
Ref: ref,
}
sRes, err := client.Stat(ctx, sReq)
if err != nil {
log.Error().Err(err).Msg("error sending grpc stat request")
w.WriteHeader(http.StatusInternalServerError)
return
}
// We also need to continue if we are not allowed to stat a resource. We may not have stat permission. That still means it exists and we need to find a new filename.
switch sRes.Status.Code {
case rpc.Code_CODE_OK, rpc.Code_CODE_PERMISSION_DENIED:
// find next filename
newName, status, err := FindName(ctx, client, filepath.Base(ref.Path), sRes.GetInfo().GetParentId())
if err != nil {
log.Error().Err(err).Msg("error sending grpc stat request")
w.WriteHeader(http.StatusInternalServerError)
return
}
if status.Code != rpc.Code_CODE_OK {
log.Error().Interface("status", status).Msg("error listing file")
errors.HandleErrorStatus(&log, w, status)
return
}
ref.Path = utils.MakeRelativePath(filepath.Join(filepath.Dir(ref.GetPath()), newName))
case rpc.Code_CODE_NOT_FOUND:
// just continue with normal upload
default:
log.Error().Interface("status", sRes.Status).Msg("error stating file")
errors.HandleErrorStatus(&log, w, sRes.Status)
return
}
}
opaque := &typespb.Opaque{}
if mtime := r.Header.Get(net.HeaderOCMtime); mtime != "" {
utils.AppendPlainToOpaque(opaque, net.HeaderOCMtime, mtime)
// TODO: find a way to check if the storage really accepted the value
w.Header().Set(net.HeaderOCMtime, "accepted")
}
if length == 0 {
tfRes, err := client.TouchFile(ctx, &provider.TouchFileRequest{
Opaque: opaque,
Ref: ref,
})
if err != nil {
log.Error().Err(err).Msg("error sending grpc touch file request")
w.WriteHeader(http.StatusInternalServerError)
return
}
if tfRes.Status.Code == rpc.Code_CODE_OK {
sRes, err := client.Stat(ctx, &provider.StatRequest{
Ref: ref,
})
if err != nil {
log.Error().Err(err).Msg("error sending grpc touch file request")
w.WriteHeader(http.StatusInternalServerError)
return
}
if sRes.Status.Code != rpc.Code_CODE_OK {
log.Error().Interface("status", sRes.Status).Msg("error touching file")
errors.HandleErrorStatus(&log, w, sRes.Status)
return
}
w.Header().Set(net.HeaderETag, sRes.Info.Etag)
w.Header().Set(net.HeaderOCETag, sRes.Info.Etag)
w.Header().Set(net.HeaderOCFileID, storagespace.FormatResourceID(sRes.Info.Id))
w.Header().Set(net.HeaderLastModified, net.RFC1123Z(sRes.Info.Mtime))
w.WriteHeader(http.StatusCreated)
return
}
if tfRes.Status.Code != rpc.Code_CODE_ALREADY_EXISTS {
log.Error().Interface("status", tfRes.Status).Msg("error touching file")
errors.HandleErrorStatus(&log, w, tfRes.Status)
return
}
}
utils.AppendPlainToOpaque(opaque, net.HeaderUploadLength, strconv.FormatInt(length, 10))
// 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(net.HeaderUploadChecksum); checksum != "" {
cparts = strings.SplitN(checksum, " ", 2)
if len(cparts) != 2 {
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(net.HeaderOCChecksum); checksum != "" {
cparts = strings.SplitN(checksum, ":", 2)
if len(cparts) != 2 {
log.Debug().Str("oc-checksum", checksum).Msg("invalid OC-Checksum format, expected '[algorithm]:[checksum]'")
w.WriteHeader(http.StatusBadRequest)
return
}
}
// 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
// algorithm is always lowercase, checksum is separated by space
utils.AppendPlainToOpaque(opaque, net.HeaderUploadChecksum, strings.ToLower(cparts[0])+" "+cparts[1])
}
uReq := &provider.InitiateFileUploadRequest{
Ref: ref,
Opaque: opaque,
LockId: requestLockToken(r),
}
if ifMatch := r.Header.Get(net.HeaderIfMatch); ifMatch != "" {
uReq.Options = &provider.InitiateFileUploadRequest_IfMatch{IfMatch: ifMatch}
}
// where to upload the file?
uRes, err := client.InitiateFileUpload(ctx, uReq)
if err != nil {
log.Error().Err(err).Msg("error initiating file upload")
w.WriteHeader(http.StatusInternalServerError)
return
}
if uRes.Status.Code != rpc.Code_CODE_OK {
if r.ProtoMajor == 1 {
// drain body to avoid `connection closed` errors
_, _ = io.Copy(io.Discard, r.Body)
}
switch uRes.Status.Code {
case rpc.Code_CODE_PERMISSION_DENIED:
status := http.StatusForbidden
m := uRes.Status.Message
// check if user has access to parent
sRes, err := client.Stat(ctx, &provider.StatRequest{Ref: &provider.Reference{
ResourceId: ref.ResourceId,
Path: utils.MakeRelativePath(path.Dir(ref.Path)),
}})
if err != nil {
log.Error().Err(err).Msg("error performing stat grpc request")
w.WriteHeader(http.StatusInternalServerError)
return
}
if sRes.Status.Code != rpc.Code_CODE_OK {
// return not found error so we do not leak existence of a file
// TODO hide permission failed for users without access in every kind of request
// TODO should this be done in the driver?
status = http.StatusNotFound
}
if status == http.StatusNotFound {
m = "Resource not found" // mimic the oc10 error message
}
w.WriteHeader(status)
b, err := errors.Marshal(status, m, "", "")
errors.HandleWebdavError(&log, w, b, err)
case rpc.Code_CODE_ABORTED:
w.WriteHeader(http.StatusPreconditionFailed)
case rpc.Code_CODE_FAILED_PRECONDITION:
w.WriteHeader(http.StatusConflict)
default:
errors.HandleErrorStatus(&log, w, uRes.Status)
}
return
}
// ony send actual PUT request if file has bytes. Otherwise the initiate file upload request creates the file
if length != 0 {
var ep, token string
for _, p := range uRes.Protocols {
if p.Protocol == "simple" {
ep, token = p.UploadEndpoint, p.Token
}
}
httpReq, err := rhttp.NewRequest(ctx, http.MethodPut, ep, r.Body)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
Propagator.Inject(ctx, propagation.HeaderCarrier(httpReq.Header))
httpReq.Header.Set(datagateway.TokenTransportHeader, token)
httpReq.ContentLength = length
httpRes, err := s.client.Do(httpReq)
if err != nil {
log.Error().Err(err).Msg("error doing PUT request to data service")
w.WriteHeader(http.StatusInternalServerError)
return
}
defer httpRes.Body.Close()
if httpRes.StatusCode != http.StatusOK {
if httpRes.StatusCode == http.StatusPartialContent {
w.WriteHeader(http.StatusPartialContent)
return
}
if httpRes.StatusCode == errtypes.StatusChecksumMismatch {
w.WriteHeader(http.StatusBadRequest)
b, err := errors.Marshal(http.StatusBadRequest, "The computed checksum does not match the one received from the client.", "", "")
errors.HandleWebdavError(&log, w, b, err)
return
}
log.Error().Err(err).Msg("PUT request to data server failed")
w.WriteHeader(httpRes.StatusCode)
return
}
// copy headers if they are present
if httpRes.Header.Get(net.HeaderETag) != "" {
w.Header().Set(net.HeaderETag, httpRes.Header.Get(net.HeaderETag))
}
if httpRes.Header.Get(net.HeaderOCETag) != "" {
w.Header().Set(net.HeaderOCETag, httpRes.Header.Get(net.HeaderOCETag))
}
if httpRes.Header.Get(net.HeaderOCFileID) != "" {
w.Header().Set(net.HeaderOCFileID, httpRes.Header.Get(net.HeaderOCFileID))
}
if httpRes.Header.Get(net.HeaderLastModified) != "" {
w.Header().Set(net.HeaderLastModified, httpRes.Header.Get(net.HeaderLastModified))
}
}
// file was new
// FIXME make created flag a property on the InitiateFileUploadResponse
if created := utils.ReadPlainFromOpaque(uRes.Opaque, "created"); created == "true" {
w.WriteHeader(http.StatusCreated)
return
}
// overwrite
w.WriteHeader(http.StatusNoContent)
}
func (s *svc) handleSpacesPut(w http.ResponseWriter, r *http.Request, spaceID string) {
ctx, span := appctx.GetTracerProvider(r.Context()).Tracer(tracerName).Start(r.Context(), "spaces_put")
defer span.End()
sublog := appctx.GetLogger(ctx).With().Str("spaceid", spaceID).Str("path", r.URL.Path).Logger()
ref, err := spacelookup.MakeStorageSpaceReference(spaceID, r.URL.Path)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
if ref.GetResourceId().GetOpaqueId() != "" && ref.GetResourceId().GetSpaceId() != ref.GetResourceId().GetOpaqueId() && r.URL.Path == "/" {
s.handlePut(ctx, w, r, &ref, sublog)
return
}
if err := ValidateName(filename(ref.Path), s.nameValidators); err != nil {
w.WriteHeader(http.StatusBadRequest)
b, err := errors.Marshal(http.StatusBadRequest, err.Error(), "", "")
errors.HandleWebdavError(&sublog, w, b, err)
return
}
s.handlePut(ctx, w, r, &ref, sublog)
}
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(r *http.Request) (int64, error) {
length, err := strconv.ParseInt(r.Header.Get(net.HeaderContentLength), 10, 64)
if err != nil {
// Fallback to Upload-Length
length, err = strconv.ParseInt(r.Header.Get(net.HeaderUploadLength), 10, 64)
if err != nil {
return 0, err
}
}
return length, nil
}
@@ -0,0 +1,32 @@
// Copyright 2018-2021 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"
"net/url"
"path"
)
func (s *svc) handleLegacyPath(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
dir := query.Get("dir")
url := s.c.PublicURL + path.Join("#", "/files/list/all", url.PathEscape(dir))
http.Redirect(w, r, url, http.StatusMovedPermanently)
}
@@ -0,0 +1,199 @@
// Copyright 2018-2021 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 (
"encoding/xml"
"io"
"net/http"
rpcv1beta1 "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
providerv1beta1 "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/net"
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/propfind"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
"github.com/opencloud-eu/reva/v2/pkg/permission"
"github.com/opencloud-eu/reva/v2/pkg/utils"
)
const (
elementNameSearchFiles = "search-files"
elementNameFilterFiles = "filter-files"
)
func (s *svc) handleReport(w http.ResponseWriter, r *http.Request, ns string) {
ctx := r.Context()
log := appctx.GetLogger(ctx)
// fn := path.Join(ns, r.URL.Path)
rep, status, err := readReport(r.Body)
if err != nil {
log.Error().Err(err).Msg("error reading report")
w.WriteHeader(status)
return
}
if rep.SearchFiles != nil {
s.doSearchFiles(w, r, rep.SearchFiles)
return
}
if rep.FilterFiles != nil {
s.doFilterFiles(w, r, rep.FilterFiles, ns)
return
}
// TODO(jfd): implement report
w.WriteHeader(http.StatusNotImplemented)
}
func (s *svc) doSearchFiles(w http.ResponseWriter, r *http.Request, sf *reportSearchFiles) {
w.WriteHeader(http.StatusNotImplemented)
}
func (s *svc) doFilterFiles(w http.ResponseWriter, r *http.Request, ff *reportFilterFiles, namespace string) {
ctx := r.Context()
log := appctx.GetLogger(ctx)
if ff.Rules.Favorite {
// List the users favorite resources.
client, err := s.gatewaySelector.Next()
if err != nil {
log.Error().Err(err).Msg("error selecting next gateway client")
w.WriteHeader(http.StatusInternalServerError)
return
}
currentUser := ctxpkg.ContextMustGetUser(ctx)
ok, err := utils.CheckPermission(ctx, permission.ListFavorites, client)
if err != nil {
log.Error().Err(err).Msg("error checking permission")
w.WriteHeader(http.StatusInternalServerError)
return
}
if !ok {
log.Info().Interface("user", currentUser).Msg("user not allowed to list favorites")
w.WriteHeader(http.StatusForbidden)
return
}
favorites, err := s.favoritesManager.ListFavorites(ctx, currentUser.Id)
if err != nil {
log.Error().Err(err).Msg("error getting favorites")
w.WriteHeader(http.StatusInternalServerError)
return
}
infos := make([]*provider.ResourceInfo, 0, len(favorites))
for i := range favorites {
statRes, err := client.Stat(ctx, &providerv1beta1.StatRequest{Ref: &providerv1beta1.Reference{ResourceId: favorites[i]}})
if err != nil {
log.Error().Err(err).Msg("error getting resource info")
continue
}
if statRes.Status.Code != rpcv1beta1.Code_CODE_OK {
log.Error().Interface("stat_response", statRes).Msg("error getting resource info")
continue
}
infos = append(infos, statRes.Info)
}
prefer := net.ParsePrefer(r.Header.Get("prefer"))
returnMinimal := prefer[net.HeaderPreferReturn] == "minimal"
responsesXML, err := propfind.MultistatusResponse(ctx, &propfind.XML{Prop: ff.Prop}, infos, s.c.PublicURL, namespace, nil, returnMinimal)
if err != nil {
log.Error().Err(err).Msg("error formatting propfind")
w.WriteHeader(http.StatusInternalServerError)
return
}
w.Header().Set(net.HeaderDav, "1, 3, extended-mkcol")
w.Header().Set(net.HeaderContentType, "application/xml; charset=utf-8")
w.Header().Set(net.HeaderVary, net.HeaderPrefer)
if returnMinimal {
w.Header().Set(net.HeaderPreferenceApplied, "return=minimal")
}
w.WriteHeader(http.StatusMultiStatus)
if _, err := w.Write(responsesXML); err != nil {
log.Err(err).Msg("error writing response")
}
}
}
type report struct {
SearchFiles *reportSearchFiles
// FilterFiles TODO add this for tag based search
FilterFiles *reportFilterFiles `xml:"filter-files"`
}
type reportSearchFiles struct {
XMLName xml.Name `xml:"search-files"`
Lang string `xml:"xml:lang,attr,omitempty"`
Prop propfind.Props `xml:"DAV: prop"`
Search reportSearchFilesSearch `xml:"search"`
}
type reportSearchFilesSearch struct {
Pattern string `xml:"search"`
Limit int `xml:"limit"`
Offset int `xml:"offset"`
}
type reportFilterFiles struct {
XMLName xml.Name `xml:"filter-files"`
Lang string `xml:"xml:lang,attr,omitempty"`
Prop propfind.Props `xml:"DAV: prop"`
Rules reportFilterFilesRules `xml:"filter-rules"`
}
type reportFilterFilesRules struct {
Favorite bool `xml:"favorite"`
SystemTag int `xml:"systemtag"`
}
func readReport(r io.Reader) (rep *report, status int, err error) {
decoder := xml.NewDecoder(r)
rep = &report{}
for {
t, err := decoder.Token()
if err == io.EOF {
// io.EOF is a successful end
return rep, 0, nil
}
if err != nil {
return nil, http.StatusBadRequest, err
}
if v, ok := t.(xml.StartElement); ok {
if v.Name.Local == elementNameSearchFiles {
var repSF reportSearchFiles
err = decoder.DecodeElement(&repSF, &v)
if err != nil {
return nil, http.StatusBadRequest, err
}
rep.SearchFiles = &repSF
} else if v.Name.Local == elementNameFilterFiles {
var repFF reportFilterFiles
err = decoder.DecodeElement(&repFF, &v)
if err != nil {
return nil, http.StatusBadRequest, err
}
rep.FilterFiles = &repFF
}
}
}
}
@@ -0,0 +1,191 @@
// Copyright 2018-2022 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 spacelookup
import (
"context"
"fmt"
"strconv"
"strings"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
storageProvider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
typesv1beta1 "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/opencloud-eu/reva/v2/pkg/storagespace"
"github.com/opencloud-eu/reva/v2/pkg/utils"
"google.golang.org/protobuf/types/known/fieldmaskpb"
)
// LookupReferenceForPath returns:
// a reference with root and relative path
// the status and error for the lookup
func LookupReferenceForPath(ctx context.Context, selector pool.Selectable[gateway.GatewayAPIClient], path string) (*storageProvider.Reference, *rpc.Status, error) {
space, cs3Status, err := LookUpStorageSpaceForPath(ctx, selector, path)
if err != nil || cs3Status.Code != rpc.Code_CODE_OK {
return nil, cs3Status, err
}
spacePath := string(space.Opaque.Map["path"].Value) // FIXME error checks
return &storageProvider.Reference{
ResourceId: space.Root,
Path: utils.MakeRelativePath(strings.TrimPrefix(path, spacePath)),
}, cs3Status, nil
}
// LookUpStorageSpaceForPath returns:
// the storage spaces responsible for a path
// the status and error for the lookup
func LookUpStorageSpaceForPath(ctx context.Context, selector pool.Selectable[gateway.GatewayAPIClient], path string) (*storageProvider.StorageSpace, *rpc.Status, error) {
// TODO add filter to only fetch spaces changed in the last 30 sec?
// TODO cache space information, invalidate after ... 5min? so we do not need to fetch all spaces?
// TODO use ListContainerStream to listen for changes
// retrieve a specific storage space
lSSReq := &storageProvider.ListStorageSpacesRequest{
Opaque: &typesv1beta1.Opaque{
Map: map[string]*typesv1beta1.OpaqueEntry{
"path": {
Decoder: "plain",
Value: []byte(path),
},
"unique": {
Decoder: "plain",
Value: []byte(strconv.FormatBool(true)),
},
},
},
}
client, err := selector.Next()
if err != nil {
return nil, status.NewInternal(ctx, "could not select next client"), err
}
lSSRes, err := client.ListStorageSpaces(ctx, lSSReq)
if err != nil || lSSRes.Status.Code != rpc.Code_CODE_OK {
status := status.NewStatusFromErrType(ctx, "failed to lookup storage spaces", err)
if lSSRes != nil {
status = lSSRes.Status
}
return nil, status, err
}
switch len(lSSRes.StorageSpaces) {
case 0:
return nil, status.NewNotFound(ctx, "no space found"), nil
case 1:
return lSSRes.StorageSpaces[0], lSSRes.Status, nil
}
return nil, status.NewInternal(ctx, "too many spaces returned"), nil
}
// LookUpStorageSpacesForPathWithChildren returns:
// the list of storage spaces responsible for a path
// the status and error for the lookup
func LookUpStorageSpacesForPathWithChildren(ctx context.Context, client gateway.GatewayAPIClient, path string) ([]*storageProvider.StorageSpace, *rpc.Status, error) {
// TODO add filter to only fetch spaces changed in the last 30 sec?
// TODO cache space information, invalidate after ... 5min? so we do not need to fetch all spaces?
// TODO use ListContainerStream to listen for changes
// retrieve a specific storage space
lSSReq := &storageProvider.ListStorageSpacesRequest{
// get all fields, including root_info
FieldMask: &fieldmaskpb.FieldMask{Paths: []string{"*"}},
}
// list all providers at or below the given path
lSSReq.Opaque = utils.AppendPlainToOpaque(lSSReq.Opaque, "path", path)
// we want to get all metadata? really? when looking up the space roots we actually only want etag, mtime and type so we can construct a child ...
lSSReq.Opaque = utils.AppendPlainToOpaque(lSSReq.Opaque, "metadata", "*")
lSSRes, err := client.ListStorageSpaces(ctx, lSSReq)
if err != nil {
return nil, nil, err
}
if lSSRes.Status.GetCode() != rpc.Code_CODE_OK {
return nil, lSSRes.Status, err
}
return lSSRes.StorageSpaces, lSSRes.Status, nil
}
// LookUpStorageSpaceByID find a space by ID
func LookUpStorageSpaceByID(ctx context.Context, client gateway.GatewayAPIClient, spaceID string) (*storageProvider.StorageSpace, *rpc.Status, error) {
// retrieve a specific storage space
lSSReq := &storageProvider.ListStorageSpacesRequest{
Opaque: &typesv1beta1.Opaque{},
Filters: []*storageProvider.ListStorageSpacesRequest_Filter{
{
Type: storageProvider.ListStorageSpacesRequest_Filter_TYPE_ID,
Term: &storageProvider.ListStorageSpacesRequest_Filter_Id{
Id: &storageProvider.StorageSpaceId{
OpaqueId: spaceID,
},
},
},
},
}
lSSRes, err := client.ListStorageSpaces(ctx, lSSReq)
if err != nil || lSSRes.Status.Code != rpc.Code_CODE_OK {
return nil, lSSRes.Status, err
}
switch len(lSSRes.StorageSpaces) {
case 0:
return nil, &rpc.Status{Code: rpc.Code_CODE_NOT_FOUND}, nil // since the caller only expects a single space return not found status
case 1:
return lSSRes.StorageSpaces[0], lSSRes.Status, nil
default:
return nil, nil, fmt.Errorf("unexpected number of spaces %d", len(lSSRes.StorageSpaces))
}
}
// MakeStorageSpaceReference find a space by id and returns a relative reference
func MakeStorageSpaceReference(spaceID string, relativePath string) (storageProvider.Reference, error) {
resourceID, err := storagespace.ParseID(spaceID)
if err != nil {
return storageProvider.Reference{}, err
}
// be tolerant about missing sharesstorageprovider id
if resourceID.StorageId == "" && resourceID.SpaceId == utils.ShareStorageSpaceID {
resourceID.StorageId = utils.ShareStorageProviderID
}
return storageProvider.Reference{
ResourceId: &resourceID,
Path: utils.MakeRelativePath(relativePath),
}, nil
}
// MakeRelativeReference returns a relative reference for the given space and path
func MakeRelativeReference(space *storageProvider.StorageSpace, relativePath string, spacesDavRequest bool) *storageProvider.Reference {
if space.Opaque == nil || space.Opaque.Map == nil || space.Opaque.Map["path"] == nil || space.Opaque.Map["path"].Decoder != "plain" {
return nil // not mounted
}
spacePath := string(space.Opaque.Map["path"].Value)
relativeSpacePath := "."
if strings.HasPrefix(relativePath, spacePath) {
relativeSpacePath = utils.MakeRelativePath(strings.TrimPrefix(relativePath, spacePath))
} else if spacesDavRequest {
relativeSpacePath = utils.MakeRelativePath(relativePath)
}
return &storageProvider.Reference{
ResourceId: space.Root,
Path: relativeSpacePath,
}
}
@@ -0,0 +1,188 @@
// Copyright 2018-2021 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"
"strings"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/config"
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/errors"
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/net"
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/propfind"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
"github.com/opencloud-eu/reva/v2/pkg/rhttp/router"
"github.com/opencloud-eu/reva/v2/pkg/storagespace"
"github.com/opencloud-eu/reva/v2/pkg/utils"
"google.golang.org/protobuf/proto"
)
// SpacesHandler handles trashbin requests
type SpacesHandler struct {
gatewaySvc string
namespace string
useLoggedInUserNS bool
}
func (h *SpacesHandler) init(c *config.Config) error {
h.gatewaySvc = c.GatewaySvc
h.namespace = path.Join("/", c.WebdavNamespace)
h.useLoggedInUserNS = true
return nil
}
// Handler handles requests
func (h *SpacesHandler) Handler(s *svc, trashbinHandler *TrashbinHandler) http.Handler {
config := s.Config()
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// ctx := r.Context()
// log := appctx.GetLogger(ctx)
if r.Method == http.MethodOptions {
s.handleOptions(w, r)
return
}
var segment string
segment, r.URL.Path = router.ShiftPath(r.URL.Path)
if segment == "" {
// listing is disabled, no auth will change that
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
if segment == _trashbinPath {
h.handleSpacesTrashbin(w, r, s, trashbinHandler)
return
}
spaceID := segment
// TODO initialize status with http.StatusBadRequest
// TODO initialize err with errors.ErrUnsupportedMethod
var status int // status 0 means the handler already sent the response
var err error
switch r.Method {
case MethodPropfind:
p := propfind.NewHandler(config.PublicURL, s.gatewaySelector, config)
p.HandleSpacesPropfind(w, r, spaceID)
case MethodProppatch:
status, err = s.handleSpacesProppatch(w, r, spaceID)
case MethodLock:
status, err = s.handleSpacesLock(w, r, spaceID)
case MethodUnlock:
status, err = s.handleSpaceUnlock(w, r, spaceID)
case MethodMkcol:
status, err = s.handleSpacesMkCol(w, r, spaceID)
case MethodMove:
s.handleSpacesMove(w, r, spaceID)
case MethodCopy:
s.handleSpacesCopy(w, r, spaceID)
case MethodReport:
s.handleReport(w, r, spaceID)
case http.MethodGet:
s.handleSpacesGet(w, r, spaceID)
case http.MethodPut:
s.handleSpacesPut(w, r, spaceID)
case http.MethodPost:
s.handleSpacesTusPost(w, r, spaceID)
case http.MethodOptions:
s.handleOptions(w, r)
case http.MethodHead:
s.handleSpacesHead(w, r, spaceID)
case http.MethodDelete:
status, err = s.handleSpacesDelete(w, r, spaceID)
default:
http.Error(w, http.StatusText(http.StatusNotImplemented), http.StatusNotImplemented)
}
if status != 0 { // 0 means the handler already sent the response
w.WriteHeader(status)
if status != http.StatusNoContent {
var b []byte
if b, err = errors.Marshal(status, err.Error(), "", ""); err == nil {
_, err = w.Write(b)
}
}
}
if err != nil {
appctx.GetLogger(r.Context()).Error().Err(err).Msg(err.Error())
}
})
}
func (h *SpacesHandler) handleSpacesTrashbin(w http.ResponseWriter, r *http.Request, s *svc, trashbinHandler *TrashbinHandler) {
ctx := r.Context()
log := appctx.GetLogger(ctx)
spaceID, key := splitSpaceAndKey(r.URL.Path)
if spaceID == "" {
// listing is disabled, no auth will change that
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
ref, err := storagespace.ParseReference(spaceID)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
switch r.Method {
case MethodPropfind:
trashbinHandler.listTrashbin(w, r, s, &ref, path.Join(_trashbinPath, spaceID), key)
case MethodMove:
if key == "" {
http.Error(w, "501 Not implemented", http.StatusNotImplemented)
break
}
// find path in url relative to trash base
baseURI := ctx.Value(net.CtxKeyBaseURI).(string)
baseURI = path.Join(baseURI, spaceID)
dh := r.Header.Get(net.HeaderDestination)
dst, err := net.ParseDestination(baseURI, dh)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
log.Debug().Str("key", key).Str("dst", dst).Msg("spaces restore")
dstRef := proto.Clone(&ref).(*provider.Reference)
dstRef.Path = utils.MakeRelativePath(dst)
trashbinHandler.restore(w, r, s, &ref, dstRef, key)
case http.MethodDelete:
trashbinHandler.delete(w, r, s, &ref, key)
default:
http.Error(w, "501 Not implemented", http.StatusNotImplemented)
}
}
func splitSpaceAndKey(p string) (space, key string) {
p = strings.TrimPrefix(p, "/")
parts := strings.SplitN(p, "/", 2)
space = parts[0]
if len(parts) > 1 {
key = parts[1]
}
return
}
@@ -0,0 +1,54 @@
// Copyright 2018-2021 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 (
"encoding/json"
"net/http"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
"github.com/opencloud-eu/reva/v2/pkg/owncloud/ocs"
)
func (s *svc) doStatus(w http.ResponseWriter, r *http.Request) {
log := appctx.GetLogger(r.Context())
status := &ocs.Status{
Installed: true,
Maintenance: false,
NeedsDBUpgrade: false,
Version: s.c.Version,
VersionString: s.c.VersionString,
Edition: s.c.Edition,
ProductName: s.c.ProductName,
ProductVersion: s.c.ProductVersion,
Product: s.c.Product,
}
statusJSON, err := json.MarshalIndent(status, "", " ")
if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
if _, err := w.Write(statusJSON); err != nil {
log.Err(err).Msg("error writing response")
}
}
@@ -0,0 +1,422 @@
// Copyright 2018-2021 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 (
"context"
"fmt"
"io"
"net/http"
"path"
"strconv"
"strings"
"time"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
typespb "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/opencloud-eu/reva/v2/internal/http/services/datagateway"
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/errors"
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/net"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/opencloud-eu/reva/v2/pkg/rhttp"
)
const (
// PerfMarkerResponseTime corresponds to the interval at which a performance marker is sent back to the TPC client
PerfMarkerResponseTime float64 = 5
)
// PerfResponse provides a single chunk of permormance marker response
type PerfResponse struct {
Timestamp time.Time
Bytes uint64
Index int
Count int
}
func (p *PerfResponse) getPerfResponseString() string {
var sb strings.Builder
sb.WriteString("Perf Marker\n")
sb.WriteString("Timestamp: " + strconv.FormatInt(p.Timestamp.Unix(), 10) + "\n")
sb.WriteString("Stripe Bytes Transferred: " + strconv.FormatUint(p.Bytes, 10) + "\n")
sb.WriteString("Strip Index: " + strconv.Itoa(p.Index) + "\n")
sb.WriteString("Total Stripe Count: " + strconv.Itoa(p.Count) + "\n")
sb.WriteString("End\n")
return sb.String()
}
// WriteCounter counts the number of bytes transferred and reports
// back to the TPC client about the progress of the transfer
// through the performance marker response stream.
type WriteCounter struct {
Total uint64
PrevTime time.Time
w http.ResponseWriter
}
// SendPerfMarker flushes a single chunk (performance marker) as
// part of the chunked transfer encoding scheme.
func (wc *WriteCounter) SendPerfMarker(size uint64) {
flusher, ok := wc.w.(http.Flusher)
if !ok {
panic("expected http.ResponseWriter to be an http.Flusher")
}
perfResp := PerfResponse{time.Now(), size, 0, 1}
pString := perfResp.getPerfResponseString()
fmt.Fprintln(wc.w, pString)
flusher.Flush()
}
func (wc *WriteCounter) Write(p []byte) (int, error) {
n := len(p)
wc.Total += uint64(n)
NowTime := time.Now()
diff := NowTime.Sub(wc.PrevTime).Seconds()
if diff >= PerfMarkerResponseTime {
wc.SendPerfMarker(wc.Total)
wc.PrevTime = NowTime
}
return n, nil
}
//
// An example of an HTTP TPC Pull
//
// +-----------------+ GET +----------------+
// | Src server | <---------------- | Dest server |
// | (Remote) | ----------------> | (Reva) |
// +-----------------+ Data +----------------+
// ^
// |
// | COPY
// |
// +----------+
// | Client |
// +----------+
// handleTPCPull performs a GET request on the remote site and upload it
// the requested reva endpoint.
func (s *svc) handleTPCPull(ctx context.Context, w http.ResponseWriter, r *http.Request, ns string) {
src := r.Header.Get("Source")
dst := path.Join(ns, r.URL.Path)
sublog := appctx.GetLogger(ctx).With().Str("src", src).Str("dst", dst).Logger()
oh := r.Header.Get(net.HeaderOverwrite)
overwrite, err := net.ParseOverwrite(oh)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
m := fmt.Sprintf("Overwrite header is set to incorrect value %v", overwrite)
sublog.Warn().Msgf("HTTP TPC Pull: %s", m)
b, err := errors.Marshal(http.StatusBadRequest, m, "", "")
errors.HandleWebdavError(&sublog, w, b, err)
return
}
sublog.Debug().Bool("overwrite", overwrite).Msg("TPC Pull")
client, err := s.gatewaySelector.Next()
if err != nil {
sublog.Error().Err(err).Msg("error selecting next gateway client")
w.WriteHeader(http.StatusInternalServerError)
return
}
// check if destination 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 {
errors.HandleErrorStatus(&sublog, w, dstStatRes.Status)
return
}
if dstStatRes.Status.Code == rpc.Code_CODE_OK && oh == "F" {
sublog.Warn().Bool("overwrite", overwrite).Msg("Destination already exists")
w.WriteHeader(http.StatusPreconditionFailed) // 412, see https://tools.ietf.org/html/rfc4918#section-9.8.5
return
}
err = s.performHTTPPull(ctx, s.gatewaySelector, r, w, ns)
if err != nil {
sublog.Error().Err(err).Msg("error performing TPC Pull")
return
}
fmt.Fprintf(w, "success: Created")
}
func (s *svc) performHTTPPull(ctx context.Context, selector pool.Selectable[gateway.GatewayAPIClient], r *http.Request, w http.ResponseWriter, ns string) error {
src := r.Header.Get("Source")
dst := path.Join(ns, r.URL.Path)
sublog := appctx.GetLogger(ctx)
sublog.Debug().Str("src", src).Str("dst", dst).Msg("Performing HTTP Pull")
// get http client for remote
httpClient := &http.Client{}
req, err := http.NewRequest("GET", src, nil)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return err
}
// add authentication header
bearerHeader := r.Header.Get(net.HeaderTransferAuth)
req.Header.Add("Authorization", bearerHeader)
// do download
httpDownloadRes, err := httpClient.Do(req) // lgtm[go/request-forgery]
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return err
}
defer httpDownloadRes.Body.Close()
if httpDownloadRes.StatusCode == http.StatusNotImplemented {
w.WriteHeader(http.StatusBadRequest)
return errtypes.NotSupported("Third-Party copy not supported, source might be a folder")
}
if httpDownloadRes.StatusCode != http.StatusOK {
w.WriteHeader(httpDownloadRes.StatusCode)
return errtypes.InternalError(fmt.Sprintf("Remote GET returned status code %d", httpDownloadRes.StatusCode))
}
client, err := s.gatewaySelector.Next()
if err != nil {
sublog.Error().Err(err).Msg("error selecting next gateway client")
w.WriteHeader(http.StatusInternalServerError)
return errtypes.InternalError(err.Error())
}
// get upload url
uReq := &provider.InitiateFileUploadRequest{
Ref: &provider.Reference{Path: dst},
Opaque: &typespb.Opaque{
Map: map[string]*typespb.OpaqueEntry{
"sizedeferred": {
Value: []byte("true"),
},
},
},
}
uRes, err := client.InitiateFileUpload(ctx, uReq)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return err
}
if uRes.Status.Code != rpc.Code_CODE_OK {
w.WriteHeader(http.StatusInternalServerError)
return fmt.Errorf("status code %d", uRes.Status.Code)
}
var uploadEP, uploadToken string
for _, p := range uRes.Protocols {
if p.Protocol == "simple" {
uploadEP, uploadToken = p.UploadEndpoint, p.Token
}
}
// send performance markers periodically every PerfMarkerResponseTime (5 seconds unless configured)
w.WriteHeader(http.StatusAccepted)
wc := WriteCounter{0, time.Now(), w}
tempReader := io.TeeReader(httpDownloadRes.Body, &wc)
// do Upload
httpUploadReq, err := rhttp.NewRequest(ctx, "PUT", uploadEP, tempReader)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return err
}
httpUploadReq.Header.Set(datagateway.TokenTransportHeader, uploadToken)
httpUploadRes, err := s.client.Do(httpUploadReq)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return err
}
defer httpUploadRes.Body.Close()
if httpUploadRes.StatusCode != http.StatusOK {
w.WriteHeader(httpUploadRes.StatusCode)
return err
}
return nil
}
//
// An example of an HTTP TPC Push
//
// +-----------------+ PUT +----------------+
// | Dest server | <---------------- | Src server |
// | (Remote) | ----------------> | (Reva) |
// +-----------------+ Done +----------------+
// ^
// |
// | COPY
// |
// +----------+
// | Client |
// +----------+
// handleTPCPush performs a PUT request on the remote site and while downloading
// data from the requested reva endpoint.
func (s *svc) handleTPCPush(ctx context.Context, w http.ResponseWriter, r *http.Request, ns string) {
src := path.Join(ns, r.URL.Path)
dst := r.Header.Get("Destination")
sublog := appctx.GetLogger(ctx).With().Str("src", src).Str("dst", dst).Logger()
oh := r.Header.Get(net.HeaderOverwrite)
overwrite, err := net.ParseOverwrite(oh)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
m := fmt.Sprintf("Overwrite header is set to incorrect value %v", overwrite)
sublog.Warn().Msgf("HTTP TPC Push: %s", m)
b, err := errors.Marshal(http.StatusBadRequest, m, "", "")
errors.HandleWebdavError(&sublog, w, b, err)
return
}
sublog.Debug().Bool("overwrite", overwrite).Msg("TPC Push")
client, err := s.gatewaySelector.Next()
if err != nil {
sublog.Error().Err(err).Msg("error selecting next gateway client")
w.WriteHeader(http.StatusInternalServerError)
return
}
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 && srcStatRes.Status.Code != rpc.Code_CODE_NOT_FOUND {
errors.HandleErrorStatus(&sublog, w, srcStatRes.Status)
return
}
if srcStatRes.Info.Type == provider.ResourceType_RESOURCE_TYPE_CONTAINER {
sublog.Error().Msg("Third-Party copy of a folder is not supported")
w.WriteHeader(http.StatusBadRequest)
return
}
err = s.performHTTPPush(ctx, r, w, srcStatRes.Info, ns)
if err != nil {
sublog.Error().Err(err).Msg("error performing TPC Push")
return
}
fmt.Fprintf(w, "success: Created")
}
func (s *svc) performHTTPPush(ctx context.Context, r *http.Request, w http.ResponseWriter, srcInfo *provider.ResourceInfo, ns string) error {
src := path.Join(ns, r.URL.Path)
dst := r.Header.Get("Destination")
sublog := appctx.GetLogger(ctx)
sublog.Debug().Str("src", src).Str("dst", dst).Msg("Performing HTTP Push")
// get download url
dReq := &provider.InitiateFileDownloadRequest{
Ref: &provider.Reference{Path: src},
}
client, err := s.gatewaySelector.Next()
if err != nil {
sublog.Error().Err(err).Msg("error selecting next gateway client")
w.WriteHeader(http.StatusInternalServerError)
return err
}
dRes, err := client.InitiateFileDownload(ctx, dReq)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return err
}
if dRes.Status.Code != rpc.Code_CODE_OK {
w.WriteHeader(http.StatusInternalServerError)
return fmt.Errorf("status code %d", dRes.Status.Code)
}
var downloadEP, downloadToken string
for _, p := range dRes.Protocols {
if p.Protocol == "simple" {
downloadEP, downloadToken = p.DownloadEndpoint, p.Token
}
}
// do download
httpDownloadReq, err := rhttp.NewRequest(ctx, "GET", downloadEP, nil)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return err
}
httpDownloadReq.Header.Set(datagateway.TokenTransportHeader, downloadToken)
httpDownloadRes, err := s.client.Do(httpDownloadReq)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return err
}
defer httpDownloadRes.Body.Close()
if httpDownloadRes.StatusCode != http.StatusOK {
w.WriteHeader(httpDownloadRes.StatusCode)
return fmt.Errorf("Remote PUT returned status code %d", httpDownloadRes.StatusCode)
}
// send performance markers periodically every PerfMarkerResponseTime (5 seconds unless configured)
w.WriteHeader(http.StatusAccepted)
wc := WriteCounter{0, time.Now(), w}
tempReader := io.TeeReader(httpDownloadRes.Body, &wc)
// get http client for a remote call
httpClient := &http.Client{}
req, err := http.NewRequest("PUT", dst, tempReader)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return err
}
// add authentication header and content length
bearerHeader := r.Header.Get(net.HeaderTransferAuth)
req.Header.Add("Authorization", bearerHeader)
req.ContentLength = int64(srcInfo.GetSize())
// do Upload
httpUploadRes, err := httpClient.Do(req) // lgtm[go/request-forgery]
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return err
}
defer httpUploadRes.Body.Close()
if httpUploadRes.StatusCode != http.StatusOK {
w.WriteHeader(httpUploadRes.StatusCode)
return err
}
return nil
}
@@ -0,0 +1,665 @@
// Copyright 2018-2021 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 (
"bytes"
"context"
"encoding/xml"
"fmt"
"net/http"
"path"
"strconv"
"strings"
"time"
"go.opentelemetry.io/otel/codes"
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/config"
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/errors"
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/net"
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/prop"
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/propfind"
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/spacelookup"
"github.com/opencloud-eu/reva/v2/pkg/storagespace"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
semconv "go.opentelemetry.io/otel/semconv/v1.20.0"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
rstatus "github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
"github.com/opencloud-eu/reva/v2/pkg/utils"
)
// TrashbinHandler handles trashbin requests
type TrashbinHandler struct {
gatewaySvc string
namespace string
allowPropfindDepthInfinitiy bool
}
func (h *TrashbinHandler) init(c *config.Config) error {
h.gatewaySvc = c.GatewaySvc
h.namespace = path.Join("/", c.FilesNamespace)
h.allowPropfindDepthInfinitiy = c.AllowPropfindDepthInfinitiy
return nil
}
// Handler handles requests
func (h *TrashbinHandler) Handler(s *svc) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
log := appctx.GetLogger(ctx)
if r.Method == http.MethodOptions {
s.handleOptions(w, r)
return
}
var username string
username, r.URL.Path = splitSpaceAndKey(r.URL.Path)
if username == "" {
// listing is disabled, no auth will change that
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
user, ok := ctxpkg.ContextGetUser(ctx)
if !ok {
w.WriteHeader(http.StatusBadRequest)
return
}
if user.Username != username {
log.Debug().Str("username", username).Interface("user", user).Msg("trying to read another users trash")
// listing other users trash is forbidden, no auth will change that
// do not leak existence of space and return 404
w.WriteHeader(http.StatusNotFound)
b, err := errors.Marshal(http.StatusNotFound, "not found", "", "")
if err != nil {
log.Error().Msgf("error marshaling xml response: %s", b)
w.WriteHeader(http.StatusInternalServerError)
return
}
_, err = w.Write(b)
if err != nil {
log.Error().Msgf("error writing xml response: %s", b)
w.WriteHeader(http.StatusInternalServerError)
return
}
return
}
useLoggedInUser := true
ns, newPath, err := s.ApplyLayout(ctx, h.namespace, useLoggedInUser, r.URL.Path)
if err != nil {
w.WriteHeader(http.StatusNotFound)
b, err := errors.Marshal(http.StatusNotFound, fmt.Sprintf("could not get storage for %s", r.URL.Path), "", "")
errors.HandleWebdavError(appctx.GetLogger(r.Context()), w, b, err)
}
r.URL.Path = newPath
basePath := path.Join(ns, newPath)
space, rpcstatus, err := spacelookup.LookUpStorageSpaceForPath(ctx, s.gatewaySelector, basePath)
switch {
case err != nil:
log.Error().Err(err).Str("path", basePath).Msg("failed to look up storage space")
w.WriteHeader(http.StatusInternalServerError)
return
case rpcstatus.Code != rpc.Code_CODE_OK:
httpStatus := rstatus.HTTPStatusFromCode(rpcstatus.Code)
w.WriteHeader(httpStatus)
b, err := errors.Marshal(httpStatus, rpcstatus.Message, "", "")
errors.HandleWebdavError(log, w, b, err)
return
}
ref := spacelookup.MakeRelativeReference(space, ".", false)
// key will be a base64 encoded cs3 path, it uniquely identifies a trash item with an opaque id and an optional path
key := r.URL.Path
switch r.Method {
case MethodPropfind:
h.listTrashbin(w, r, s, ref, user.Username, key)
case MethodMove:
if key == "" {
http.Error(w, "501 Not implemented", http.StatusNotImplemented)
break
}
// find path in url relative to trash base
trashBase := ctx.Value(net.CtxKeyBaseURI).(string)
baseURI := path.Join(path.Dir(trashBase), "files", username)
dh := r.Header.Get(net.HeaderDestination)
dst, err := net.ParseDestination(baseURI, dh)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
p := path.Join(ns, dst)
// The destination can be in another space. E.g. the 'Shares Jail'.
space, rpcstatus, err := spacelookup.LookUpStorageSpaceForPath(ctx, s.gatewaySelector, p)
if err != nil {
log.Error().Err(err).Str("path", p).Msg("failed to look up destination storage space")
w.WriteHeader(http.StatusInternalServerError)
return
}
if rpcstatus.Code != rpc.Code_CODE_OK {
httpStatus := rstatus.HTTPStatusFromCode(rpcstatus.Code)
w.WriteHeader(httpStatus)
b, err := errors.Marshal(httpStatus, rpcstatus.Message, "", "")
errors.HandleWebdavError(log, w, b, err)
return
}
dstRef := spacelookup.MakeRelativeReference(space, p, false)
log.Debug().Str("key", key).Str("dst", dst).Msg("restore")
h.restore(w, r, s, ref, dstRef, key)
case http.MethodDelete:
h.delete(w, r, s, ref, key)
default:
http.Error(w, "501 Not implemented", http.StatusNotImplemented)
}
})
}
func (h *TrashbinHandler) getDepth(r *http.Request) (net.Depth, error) {
dh := r.Header.Get(net.HeaderDepth)
depth, err := net.ParseDepth(dh)
if err != nil || depth == net.DepthInfinity && !h.allowPropfindDepthInfinitiy {
return "", errors.ErrInvalidDepth
}
return depth, nil
}
func (h *TrashbinHandler) listTrashbin(w http.ResponseWriter, r *http.Request, s *svc, ref *provider.Reference, refBase, key string) {
ctx, span := appctx.GetTracerProvider(r.Context()).Tracer(tracerName).Start(r.Context(), "list_trashbin")
defer span.End()
sublog := appctx.GetLogger(ctx).With().Logger()
depth, err := h.getDepth(r)
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, "Invalid Depth header value")
span.SetAttributes(semconv.HTTPStatusCodeKey.Int(http.StatusBadRequest))
sublog.Debug().Str("depth", r.Header.Get(net.HeaderDepth)).Msg(err.Error())
w.WriteHeader(http.StatusBadRequest)
m := fmt.Sprintf("Invalid Depth header value: %v", r.Header.Get(net.HeaderDepth))
b, err := errors.Marshal(http.StatusBadRequest, m, "", "")
errors.HandleWebdavError(&sublog, w, b, err)
return
}
pf, status, err := propfind.ReadPropfind(r.Body)
if err != nil {
sublog.Debug().Err(err).Msg("error reading propfind request")
w.WriteHeader(status)
return
}
if key == "" && depth == net.DepthZero {
// we are listing the trash root, but without children
// so we just fake a root element without actually querying the gateway
rootHref := path.Join(refBase, key)
propRes, err := h.formatTrashPropfind(ctx, s, ref.ResourceId.SpaceId, refBase, rootHref, &pf, nil, true)
if err != nil {
sublog.Error().Err(err).Msg("error formatting propfind")
w.WriteHeader(http.StatusInternalServerError)
return
}
w.Header().Set(net.HeaderDav, "1, 3, extended-mkcol")
w.Header().Set(net.HeaderContentType, "application/xml; charset=utf-8")
w.WriteHeader(http.StatusMultiStatus)
_, err = w.Write(propRes)
if err != nil {
sublog.Error().Err(err).Msg("error writing body")
return
}
return
}
if depth == net.DepthOne && key != "" && !strings.HasSuffix(key, "/") {
// when a key is provided and the depth is 1 we need to append a / to the key to list the children
key += "/"
}
client, err := s.gatewaySelector.Next()
if err != nil {
sublog.Error().Err(err).Msg("error selecting next gateway client")
w.WriteHeader(http.StatusInternalServerError)
return
}
// ask gateway for recycle items
getRecycleRes, err := client.ListRecycle(ctx, &provider.ListRecycleRequest{Ref: ref, Key: key})
if err != nil {
sublog.Error().Err(err).Msg("error calling ListRecycle")
w.WriteHeader(http.StatusInternalServerError)
return
}
if getRecycleRes.Status.Code != rpc.Code_CODE_OK {
httpStatus := rstatus.HTTPStatusFromCode(getRecycleRes.Status.Code)
w.WriteHeader(httpStatus)
b, err := errors.Marshal(httpStatus, getRecycleRes.Status.Message, "", "")
errors.HandleWebdavError(&sublog, w, b, err)
return
}
items := getRecycleRes.RecycleItems
if depth == net.DepthInfinity {
var stack []string
// check sub-containers in reverse order and add them to the stack
// the reversed order here will produce a more logical sorting of results
for i := len(items) - 1; i >= 0; i-- {
// for i := range res.Infos {
if items[i].Type == provider.ResourceType_RESOURCE_TYPE_CONTAINER {
stack = append(stack, items[i].Key+"/") // fetch children of the item
}
}
for len(stack) > 0 {
key := stack[len(stack)-1]
getRecycleRes, err := client.ListRecycle(ctx, &provider.ListRecycleRequest{Ref: ref, Key: key})
if err != nil {
sublog.Error().Err(err).Msg("error calling ListRecycle")
w.WriteHeader(http.StatusInternalServerError)
return
}
if getRecycleRes.Status.Code != rpc.Code_CODE_OK {
httpStatus := rstatus.HTTPStatusFromCode(getRecycleRes.Status.Code)
w.WriteHeader(httpStatus)
b, err := errors.Marshal(httpStatus, getRecycleRes.Status.Message, "", "")
errors.HandleWebdavError(&sublog, w, b, err)
return
}
items = append(items, getRecycleRes.RecycleItems...)
stack = stack[:len(stack)-1]
// check sub-containers in reverse order and add them to the stack
// the reversed order here will produce a more logical sorting of results
for i := len(getRecycleRes.RecycleItems) - 1; i >= 0; i-- {
// for i := range res.Infos {
if getRecycleRes.RecycleItems[i].Type == provider.ResourceType_RESOURCE_TYPE_CONTAINER {
stack = append(stack, getRecycleRes.RecycleItems[i].Key)
}
}
}
}
rootHref := path.Join(refBase, key)
propRes, err := h.formatTrashPropfind(ctx, s, ref.ResourceId.SpaceId, refBase, rootHref, &pf, items, depth != net.DepthZero)
if err != nil {
sublog.Error().Err(err).Msg("error formatting propfind")
w.WriteHeader(http.StatusInternalServerError)
return
}
w.Header().Set(net.HeaderDav, "1, 3, extended-mkcol")
w.Header().Set(net.HeaderContentType, "application/xml; charset=utf-8")
w.WriteHeader(http.StatusMultiStatus)
_, err = w.Write(propRes)
if err != nil {
sublog.Error().Err(err).Msg("error writing body")
return
}
}
func (h *TrashbinHandler) formatTrashPropfind(ctx context.Context, s *svc, spaceID, refBase, rootHref string, pf *propfind.XML, items []*provider.RecycleItem, fakeRoot bool) ([]byte, error) {
responses := make([]*propfind.ResponseXML, 0, len(items)+1)
if fakeRoot {
responses = append(responses, &propfind.ResponseXML{
Href: net.EncodePath(path.Join(ctx.Value(net.CtxKeyBaseURI).(string), rootHref) + "/"), // url encode response.Href TODO
Propstat: []propfind.PropstatXML{
{
Status: "HTTP/1.1 200 OK",
Prop: []prop.PropertyXML{
prop.Raw("d:resourcetype", "<d:collection/>"),
},
},
{
Status: "HTTP/1.1 404 Not Found",
Prop: []prop.PropertyXML{
prop.NotFound("oc:trashbin-original-filename"),
prop.NotFound("oc:trashbin-original-location"),
prop.NotFound("oc:trashbin-delete-datetime"),
prop.NotFound("d:getcontentlength"),
},
},
},
})
}
for i := range items {
res, err := h.itemToPropResponse(ctx, s, spaceID, refBase, pf, items[i])
if err != nil {
return nil, err
}
responses = append(responses, res)
}
responsesXML, err := xml.Marshal(&responses)
if err != nil {
return nil, err
}
var buf bytes.Buffer
buf.WriteString(`<?xml version="1.0" encoding="utf-8"?><d:multistatus xmlns:d="DAV:" `)
buf.WriteString(`xmlns:s="http://sabredav.org/ns" xmlns:oc="http://owncloud.org/ns">`)
buf.Write(responsesXML)
buf.WriteString(`</d:multistatus>`)
return buf.Bytes(), nil
}
// itemToPropResponse needs to create a listing that contains a key and destination
// the key is the name of an entry in the trash listing
// for now we need to limit trash to the users home, so we can expect all trash keys to have the home storage as the opaque id
func (h *TrashbinHandler) itemToPropResponse(ctx context.Context, s *svc, spaceID, refBase string, pf *propfind.XML, item *provider.RecycleItem) (*propfind.ResponseXML, error) {
baseURI := ctx.Value(net.CtxKeyBaseURI).(string)
ref := path.Join(baseURI, refBase, item.GetKey())
if item.GetType() == provider.ResourceType_RESOURCE_TYPE_CONTAINER {
ref += "/"
}
response := propfind.ResponseXML{
Href: net.EncodePath(ref), // url encode response.Href
Propstat: []propfind.PropstatXML{},
}
// TODO(jfd): if the path we list here is taken from the ListRecycle request we rely on the gateway to prefix it with the mount point
t := utils.TSToTime(item.GetDeletionTime()).UTC()
dTime := t.Format(time.RFC1123Z)
size := strconv.FormatUint(item.GetSize(), 10)
// when allprops has been requested
if pf.Allprop != nil {
// return all known properties
propstatOK := propfind.PropstatXML{
Status: "HTTP/1.1 200 OK",
Prop: []prop.PropertyXML{},
}
// yes this is redundant, can be derived from oc:trashbin-original-location which contains the full path, clients should not fetch it
propstatOK.Prop = append(propstatOK.Prop, prop.Escaped("oc:trashbin-original-filename", path.Base(item.GetRef().GetPath())))
propstatOK.Prop = append(propstatOK.Prop, prop.Escaped("oc:trashbin-original-location", strings.TrimPrefix(item.GetRef().GetPath(), "/")))
propstatOK.Prop = append(propstatOK.Prop, prop.Escaped("oc:trashbin-delete-timestamp", strconv.FormatUint(item.GetDeletionTime().GetSeconds(), 10)))
propstatOK.Prop = append(propstatOK.Prop, prop.Escaped("oc:trashbin-delete-datetime", dTime))
if item.GetType() == provider.ResourceType_RESOURCE_TYPE_CONTAINER {
propstatOK.Prop = append(propstatOK.Prop, prop.Raw("d:resourcetype", "<d:collection/>"))
propstatOK.Prop = append(propstatOK.Prop, prop.Raw("oc:size", size))
} else {
propstatOK.Prop = append(propstatOK.Prop,
prop.Escaped("d:resourcetype", ""),
prop.Escaped("d:getcontentlength", size),
)
}
propstatOK.Prop = append(propstatOK.Prop, prop.Escaped("oc:spaceid", spaceID))
response.Propstat = append(response.Propstat, propstatOK)
} else {
// otherwise return only the requested properties
propstatOK := propfind.PropstatXML{
Status: "HTTP/1.1 200 OK",
Prop: []prop.PropertyXML{},
}
propstatNotFound := propfind.PropstatXML{
Status: "HTTP/1.1 404 Not Found",
Prop: []prop.PropertyXML{},
}
for i := range pf.Prop {
switch pf.Prop[i].Space {
case net.NsOwncloud:
switch pf.Prop[i].Local {
case "oc:size":
if item.GetType() == provider.ResourceType_RESOURCE_TYPE_CONTAINER {
propstatOK.Prop = append(propstatOK.Prop, prop.Escaped("oc:size", size))
} else {
propstatNotFound.Prop = append(propstatNotFound.Prop, prop.NotFound("oc:size"))
}
case "trashbin-original-filename":
// yes this is redundant, can be derived from oc:trashbin-original-location which contains the full path, clients should not fetch it
propstatOK.Prop = append(propstatOK.Prop, prop.Escaped("oc:trashbin-original-filename", path.Base(item.GetRef().GetPath())))
case "trashbin-original-location":
// TODO (jfd) double check and clarify the cs3 spec what the Key is about and if Path is only the folder that contains the file or if it includes the filename
propstatOK.Prop = append(propstatOK.Prop, prop.Escaped("oc:trashbin-original-location", strings.TrimPrefix(item.GetRef().GetPath(), "/")))
case "trashbin-delete-datetime":
propstatOK.Prop = append(propstatOK.Prop, prop.Escaped("oc:trashbin-delete-datetime", dTime))
case "trashbin-delete-timestamp":
propstatOK.Prop = append(propstatOK.Prop, prop.Escaped("oc:trashbin-delete-timestamp", strconv.FormatUint(item.GetDeletionTime().GetSeconds(), 10)))
case "spaceid":
propstatOK.Prop = append(propstatOK.Prop, prop.Escaped("oc:spaceid", spaceID))
default:
propstatNotFound.Prop = append(propstatNotFound.Prop, prop.NotFound("oc:"+pf.Prop[i].Local))
}
case net.NsDav:
switch pf.Prop[i].Local {
case "getcontentlength":
if item.GetType() == provider.ResourceType_RESOURCE_TYPE_CONTAINER {
propstatNotFound.Prop = append(propstatNotFound.Prop, prop.NotFound("d:getcontentlength"))
} else {
propstatOK.Prop = append(propstatOK.Prop, prop.Escaped("d:getcontentlength", size))
}
case "resourcetype":
if item.GetType() == provider.ResourceType_RESOURCE_TYPE_CONTAINER {
propstatOK.Prop = append(propstatOK.Prop, prop.Raw("d:resourcetype", "<d:collection/>"))
} else {
propstatOK.Prop = append(propstatOK.Prop, prop.Raw("d:resourcetype", ""))
// redirectref is another option
}
case "getcontenttype":
if item.GetType() == provider.ResourceType_RESOURCE_TYPE_CONTAINER {
propstatOK.Prop = append(propstatOK.Prop, prop.Raw("d:getcontenttype", "httpd/unix-directory"))
} else {
propstatNotFound.Prop = append(propstatNotFound.Prop, prop.NotFound("d:getcontenttype"))
}
default:
propstatNotFound.Prop = append(propstatNotFound.Prop, prop.NotFound("d:"+pf.Prop[i].Local))
}
default:
// TODO (jfd) lookup shortname for unknown namespaces?
propstatNotFound.Prop = append(propstatNotFound.Prop, prop.NotFound(pf.Prop[i].Space+":"+pf.Prop[i].Local))
}
}
response.Propstat = append(response.Propstat, propstatOK, propstatNotFound)
}
return &response, nil
}
func (h *TrashbinHandler) restore(w http.ResponseWriter, r *http.Request, s *svc, ref, dst *provider.Reference, key string) {
ctx, span := appctx.GetTracerProvider(r.Context()).Tracer(tracerName).Start(r.Context(), "restore")
defer span.End()
sublog := appctx.GetLogger(ctx).With().Logger()
oh := r.Header.Get(net.HeaderOverwrite)
overwrite, err := net.ParseOverwrite(oh)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
client, err := s.gatewaySelector.Next()
if err != nil {
sublog.Error().Err(err).Msg("error selecting next gateway client")
w.WriteHeader(http.StatusInternalServerError)
return
}
dstStatReq := &provider.StatRequest{Ref: dst}
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 {
errors.HandleErrorStatus(&sublog, w, dstStatRes.Status)
return
}
// Restoring to a non-existent location is not supported by the WebDAV spec. The following block ensures the target
// restore location exists, and if it doesn't returns a conflict error code.
if dstStatRes.Status.Code == rpc.Code_CODE_NOT_FOUND && isNested(dst.Path) {
parentRef := &provider.Reference{ResourceId: dst.ResourceId, Path: utils.MakeRelativePath(path.Dir(dst.Path))}
parentStatReq := &provider.StatRequest{Ref: parentRef}
parentStatResponse, err := client.Stat(ctx, parentStatReq)
if err != nil {
sublog.Error().Err(err).Msg("error sending grpc stat request")
w.WriteHeader(http.StatusInternalServerError)
return
}
if parentStatResponse.Status.Code == rpc.Code_CODE_NOT_FOUND {
// 409 if intermediate dir is missing, see https://tools.ietf.org/html/rfc4918#section-9.8.5
w.WriteHeader(http.StatusConflict)
return
}
}
successCode := http.StatusCreated // 201 if new resource was created, see https://tools.ietf.org/html/rfc4918#section-9.9.4
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.9.4
if !overwrite {
sublog.Warn().Bool("overwrite", overwrite).Msg("dst already exists")
w.WriteHeader(http.StatusPreconditionFailed) // 412, see https://tools.ietf.org/html/rfc4918#section-9.9.4
b, err := errors.Marshal(
http.StatusPreconditionFailed,
"The destination node already exists, and the overwrite header is set to false",
net.HeaderOverwrite,
"",
)
errors.HandleWebdavError(&sublog, w, b, err)
return
}
// delete existing tree
delReq := &provider.DeleteRequest{Ref: dst}
delRes, err := client.Delete(ctx, delReq)
if err != nil {
sublog.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 {
errors.HandleErrorStatus(&sublog, w, delRes.Status)
return
}
}
req := &provider.RestoreRecycleItemRequest{
Ref: ref,
Key: key,
RestoreRef: dst,
}
res, err := client.RestoreRecycleItem(ctx, req)
if err != nil {
sublog.Error().Err(err).Msg("error sending a grpc restore recycle item request")
w.WriteHeader(http.StatusInternalServerError)
return
}
if res.Status.Code != rpc.Code_CODE_OK {
if res.Status.Code == rpc.Code_CODE_PERMISSION_DENIED {
w.WriteHeader(http.StatusForbidden)
b, err := errors.Marshal(http.StatusForbidden, "Permission denied to restore", "", "")
errors.HandleWebdavError(&sublog, w, b, err)
}
errors.HandleErrorStatus(&sublog, w, res.Status)
return
}
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 {
errors.HandleErrorStatus(&sublog, w, dstStatRes.Status)
return
}
info := dstStatRes.Info
w.Header().Set(net.HeaderContentType, info.MimeType)
w.Header().Set(net.HeaderETag, info.Etag)
w.Header().Set(net.HeaderOCFileID, storagespace.FormatResourceID(info.Id))
w.Header().Set(net.HeaderOCETag, info.Etag)
w.WriteHeader(successCode)
}
// delete has only a key
func (h *TrashbinHandler) delete(w http.ResponseWriter, r *http.Request, s *svc, ref *provider.Reference, key string) {
ctx, span := appctx.GetTracerProvider(r.Context()).Tracer(tracerName).Start(r.Context(), "erase")
defer span.End()
sublog := appctx.GetLogger(ctx).With().Interface("reference", ref).Str("key", key).Logger()
req := &provider.PurgeRecycleRequest{
Ref: ref,
Key: key,
}
client, err := s.gatewaySelector.Next()
if err != nil {
sublog.Error().Err(err).Msg("error selecting next gateway client")
w.WriteHeader(http.StatusInternalServerError)
return
}
res, err := client.PurgeRecycle(ctx, req)
if err != nil {
sublog.Error().Err(err).Msg("error sending a grpc restore recycle item request")
w.WriteHeader(http.StatusInternalServerError)
return
}
switch res.Status.Code {
case rpc.Code_CODE_OK:
w.WriteHeader(http.StatusNoContent)
case rpc.Code_CODE_NOT_FOUND:
sublog.Debug().Interface("status", res.Status).Msg("resource not found")
w.WriteHeader(http.StatusConflict)
m := fmt.Sprintf("key %s not found", key)
b, err := errors.Marshal(http.StatusConflict, m, "", "")
errors.HandleWebdavError(&sublog, w, b, err)
case rpc.Code_CODE_PERMISSION_DENIED:
w.WriteHeader(http.StatusForbidden)
var m string
if key == "" {
m = "Permission denied to purge recycle"
} else {
m = "Permission denied to delete"
}
b, err := errors.Marshal(http.StatusForbidden, m, "", "")
errors.HandleWebdavError(&sublog, w, b, err)
default:
errors.HandleErrorStatus(&sublog, w, res.Status)
}
}
func isNested(p string) bool {
dir, _ := path.Split(p)
return dir != "/" && dir != "./"
}
@@ -0,0 +1,393 @@
// Copyright 2018-2021 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 (
"context"
"encoding/json"
"io"
"net/http"
"path"
"path/filepath"
"strconv"
"strings"
"time"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
link "github.com/cs3org/go-cs3apis/cs3/sharing/link/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
typespb "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/errors"
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/net"
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/spacelookup"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
"github.com/opencloud-eu/reva/v2/pkg/conversions"
"github.com/opencloud-eu/reva/v2/pkg/rhttp"
"github.com/opencloud-eu/reva/v2/pkg/storagespace"
"github.com/opencloud-eu/reva/v2/pkg/utils"
"github.com/rs/zerolog"
tusd "github.com/tus/tusd/v2/pkg/handler"
"go.opentelemetry.io/otel/propagation"
)
// Propagator ensures the importer module uses the same trace propagation strategy.
var Propagator = propagation.NewCompositeTextMapPropagator(
propagation.Baggage{},
propagation.TraceContext{},
)
func (s *svc) handlePathTusPost(w http.ResponseWriter, r *http.Request, ns string) {
ctx, span := appctx.GetTracerProvider(r.Context()).Tracer(tracerName).Start(r.Context(), "tus-post")
defer span.End()
// read filename from metadata
meta := tusd.ParseMetadataHeader(r.Header.Get(net.HeaderUploadMetadata))
// append filename to current dir
ref := &provider.Reference{
// a path based request has no resource id, so we can only provide a path. The gateway has te figure out which provider is responsible
Path: path.Join(ns, r.URL.Path, meta["filename"]),
}
sublog := appctx.GetLogger(ctx).With().Str("path", r.URL.Path).Str("filename", meta["filename"]).Logger()
s.handleTusPost(ctx, w, r, meta, ref, sublog)
}
func (s *svc) handleSpacesTusPost(w http.ResponseWriter, r *http.Request, spaceID string) {
ctx, span := appctx.GetTracerProvider(r.Context()).Tracer(tracerName).Start(r.Context(), "spaces-tus-post")
defer span.End()
// read filename from metadata
meta := tusd.ParseMetadataHeader(r.Header.Get(net.HeaderUploadMetadata))
ref, err := spacelookup.MakeStorageSpaceReference(spaceID, path.Join(r.URL.Path, meta["filename"]))
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
sublog := appctx.GetLogger(ctx).With().Str("spaceid", spaceID).Str("path", r.URL.Path).Str("filename", meta["filename"]).Logger()
s.handleTusPost(ctx, w, r, meta, &ref, sublog)
}
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(net.HeaderAccessControlAllowHeaders, strings.Join([]string{net.HeaderTusResumable, net.HeaderUploadLength, net.HeaderUploadMetadata, net.HeaderIfMatch}, ", "))
w.Header().Add(net.HeaderAccessControlExposeHeaders, strings.Join([]string{net.HeaderTusResumable, net.HeaderUploadOffset, net.HeaderLocation}, ", "))
w.Header().Set(net.HeaderTusExtension, "creation,creation-with-upload,checksum,expiration")
w.Header().Set(net.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(net.HeaderTusResumable) != "1.0.0" {
w.WriteHeader(http.StatusPreconditionFailed)
return
}
if r.Header.Get(net.HeaderUploadLength) == "" {
w.WriteHeader(http.StatusPreconditionFailed)
return
}
if err := ValidateName(filename(meta["filename"]), s.nameValidators); err != nil {
w.WriteHeader(http.StatusPreconditionFailed)
return
}
// Test if the target is a secret filedrop
var isSecretFileDrop bool
tokenStatInfo, ok := TokenStatInfoFromContext(ctx)
// We assume that when the uploader can create containers, but is not allowed to list them, it is a secret file drop
if ok && tokenStatInfo.GetPermissionSet().CreateContainer && !tokenStatInfo.GetPermissionSet().ListContainer {
isSecretFileDrop = true
}
// r.Header.Get(net.HeaderOCChecksum)
// TODO must be SHA1, ADLER32 or MD5 ... in capital letters????
// curl -X PUT https://demo.owncloud.com/remote.php/webdav/testcs.bin -u demo:demo -d '123' -v -H 'OC-Checksum: SHA1:40bd001563085fc35165329ea1ff5c5ecbdbbeef'
// TODO check Expect: 100-continue
client, err := s.gatewaySelector.Next()
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
sReq := &provider.StatRequest{
Ref: ref,
}
sRes, err := client.Stat(ctx, sReq)
if 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 && sRes.Status.Code != rpc.Code_CODE_NOT_FOUND {
errors.HandleErrorStatus(&log, w, sRes.Status)
return
}
info := sRes.Info
if info != nil && info.Type != provider.ResourceType_RESOURCE_TYPE_FILE {
log.Warn().Msg("resource is not a file")
w.WriteHeader(http.StatusConflict)
return
}
if info != nil {
clientETag := r.Header.Get(net.HeaderIfMatch)
serverETag := info.Etag
if clientETag != "" {
if clientETag != serverETag {
log.Warn().Str("client-etag", clientETag).Str("server-etag", serverETag).Msg("etags mismatch")
w.WriteHeader(http.StatusPreconditionFailed)
return
}
}
if isSecretFileDrop {
// find next filename
newName, status, err := FindName(ctx, client, filepath.Base(ref.Path), sRes.GetInfo().GetParentId())
if err != nil {
log.Error().Err(err).Msg("error sending grpc stat request")
w.WriteHeader(http.StatusInternalServerError)
return
}
if status.GetCode() != rpc.Code_CODE_OK {
log.Error().Interface("status", status).Msg("error listing file")
errors.HandleErrorStatus(&log, w, status)
return
}
ref.Path = filepath.Join(filepath.Dir(ref.GetPath()), newName)
sRes.GetInfo().Name = newName
}
}
uploadLength, err := strconv.ParseInt(r.Header.Get(net.HeaderUploadLength), 10, 64)
if err != nil {
log.Debug().Err(err).Msg("wrong request")
w.WriteHeader(http.StatusBadRequest)
return
}
if uploadLength == 0 {
tfRes, err := client.TouchFile(ctx, &provider.TouchFileRequest{
Ref: ref,
})
if err != nil {
log.Error().Err(err).Msg("error sending grpc stat request")
w.WriteHeader(http.StatusInternalServerError)
return
}
switch tfRes.Status.Code {
case rpc.Code_CODE_OK:
w.Header().Set(net.HeaderLocation, "")
w.WriteHeader(http.StatusCreated)
return
case rpc.Code_CODE_ALREADY_EXISTS:
// Fall through to the tus case
default:
log.Error().Interface("status", tfRes.Status).Msg("error touching file")
w.WriteHeader(http.StatusInternalServerError)
return
}
}
opaqueMap := map[string]*typespb.OpaqueEntry{
net.HeaderUploadLength: {
Decoder: "plain",
Value: []byte(r.Header.Get(net.HeaderUploadLength)),
},
}
mtime := meta["mtime"]
if mtime != "" {
opaqueMap[net.HeaderOCMtime] = &typespb.OpaqueEntry{
Decoder: "plain",
Value: []byte(mtime),
}
}
// initiateUpload
uReq := &provider.InitiateFileUploadRequest{
Ref: ref,
Opaque: &typespb.Opaque{
Map: opaqueMap,
},
}
uRes, err := client.InitiateFileUpload(ctx, uReq)
if err != nil {
log.Error().Err(err).Msg("error initiating file upload")
w.WriteHeader(http.StatusInternalServerError)
return
}
if uRes.Status.Code != rpc.Code_CODE_OK {
if r.ProtoMajor == 1 {
// drain body to avoid `connection closed` errors
_, _ = io.Copy(io.Discard, r.Body)
}
if uRes.Status.Code == rpc.Code_CODE_NOT_FOUND {
w.WriteHeader(http.StatusPreconditionFailed)
return
}
errors.HandleErrorStatus(&log, w, uRes.Status)
return
}
var ep, token string
for _, p := range uRes.Protocols {
if p.Protocol == "tus" {
ep, token = p.UploadEndpoint, p.Token
}
}
// TUS clients don't understand the reva transfer token. We need to append it to the upload endpoint.
// The DataGateway has to take care of pulling it back into the request header upon request arrival.
if token != "" {
if !strings.HasSuffix(ep, "/") {
ep += "/"
}
ep += token
}
w.Header().Set(net.HeaderLocation, ep)
// for creation-with-upload extension forward bytes to dataprovider
// TODO check this really streams
if r.Header.Get(net.HeaderContentType) == "application/offset+octet-stream" {
finishUpload := true
if uploadLength > 0 {
var httpRes *http.Response
httpReq, err := rhttp.NewRequest(ctx, http.MethodPatch, ep, r.Body)
if err != nil {
log.Debug().Err(err).Msg("wrong request")
w.WriteHeader(http.StatusInternalServerError)
return
}
Propagator.Inject(ctx, propagation.HeaderCarrier(httpReq.Header))
httpReq.Header.Set(net.HeaderContentType, r.Header.Get(net.HeaderContentType))
httpReq.Header.Set(net.HeaderContentLength, r.Header.Get(net.HeaderContentLength))
if r.Header.Get(net.HeaderUploadOffset) != "" {
httpReq.Header.Set(net.HeaderUploadOffset, r.Header.Get(net.HeaderUploadOffset))
} else {
httpReq.Header.Set(net.HeaderUploadOffset, "0")
}
httpReq.Header.Set(net.HeaderTusResumable, r.Header.Get(net.HeaderTusResumable))
httpRes, err = s.client.Do(httpReq)
if err != nil || httpRes == nil {
log.Error().Err(err).Msg("error doing PATCH request to data gateway")
w.WriteHeader(http.StatusInternalServerError)
return
}
defer httpRes.Body.Close()
if httpRes.StatusCode != http.StatusNoContent {
w.WriteHeader(httpRes.StatusCode)
return
}
w.Header().Set(net.HeaderUploadOffset, httpRes.Header.Get(net.HeaderUploadOffset))
w.Header().Set(net.HeaderTusResumable, httpRes.Header.Get(net.HeaderTusResumable))
w.Header().Set(net.HeaderTusUploadExpires, httpRes.Header.Get(net.HeaderTusUploadExpires))
if httpRes.Header.Get(net.HeaderOCMtime) != "" {
w.Header().Set(net.HeaderOCMtime, httpRes.Header.Get(net.HeaderOCMtime))
}
if strings.HasPrefix(uReq.GetRef().GetPath(), "/public") && uReq.GetRef().GetResourceId() == nil {
// Use the path based request for the public link
sReq.Ref.Path = uReq.Ref.GetPath()
sReq.Ref.ResourceId = nil
} else {
if resid, err := storagespace.ParseID(httpRes.Header.Get(net.HeaderOCFileID)); err == nil {
sReq.Ref = &provider.Reference{
ResourceId: &resid,
}
}
}
finishUpload = httpRes.Header.Get(net.HeaderUploadOffset) == r.Header.Get(net.HeaderUploadLength)
}
// check if upload was fully completed
if uploadLength == 0 || finishUpload {
// get uploaded file metadata
sRes, err := client.Stat(ctx, sReq)
if 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 && sRes.Status.Code != rpc.Code_CODE_NOT_FOUND {
if sRes.Status.Code == rpc.Code_CODE_PERMISSION_DENIED {
// the token expired during upload, so the stat failed
// and we can't do anything about it.
// the clients will handle this gracefully by doing a propfind on the file
w.WriteHeader(http.StatusOK)
return
}
errors.HandleErrorStatus(&log, w, sRes.Status)
return
}
info := sRes.Info
if info == nil {
log.Error().Msg("No info found for uploaded file")
w.WriteHeader(http.StatusInternalServerError)
return
}
// get WebDav permissions for file
isPublic := false
if info.Opaque != nil && info.Opaque.Map != nil {
if info.Opaque.Map["link-share"] != nil && info.Opaque.Map["link-share"].Decoder == "json" {
ls := &link.PublicShare{}
_ = json.Unmarshal(info.Opaque.Map["link-share"].Value, ls)
isPublic = ls != nil
}
}
isShared := !net.IsCurrentUserOwnerOrManager(ctx, info.Owner, info)
role := conversions.RoleFromResourcePermissions(info.PermissionSet, isPublic)
permissions := role.WebDAVPermissions(
info.Type == provider.ResourceType_RESOURCE_TYPE_CONTAINER,
isShared,
false,
isPublic,
)
w.Header().Set(net.HeaderContentType, info.MimeType)
w.Header().Set(net.HeaderOCFileID, storagespace.FormatResourceID(info.Id))
w.Header().Set(net.HeaderOCETag, info.Etag)
w.Header().Set(net.HeaderETag, info.Etag)
w.Header().Set(net.HeaderOCPermissions, permissions)
t := utils.TSToTime(info.Mtime).UTC()
lastModifiedString := t.Format(time.RFC1123Z)
w.Header().Set(net.HeaderLastModified, lastModifiedString)
}
}
w.WriteHeader(http.StatusCreated)
}
@@ -0,0 +1,79 @@
package ocdav
import (
"errors"
"fmt"
"strings"
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/config"
)
// Validator validates strings
type Validator func(string) error
// ValidatorsFromConfig returns the configured Validators
func ValidatorsFromConfig(c *config.Config) []Validator {
// we always want to exclude empty names
vals := []Validator{notEmpty()}
// forbidden characters
vals = append(vals, doesNotContain(c.NameValidation.InvalidChars))
// max length
vals = append(vals, isShorterThan(c.NameValidation.MaxLength))
return vals
}
// ValidateName will validate a file or folder name, returning an error when it is not accepted
func ValidateName(name string, validators []Validator) error {
return ValidateDestination(name, append(validators, notReserved()))
}
// ValidateDestination will validate a file or folder destination name (which can be . or ..), returning an error when it is not accepted
func ValidateDestination(name string, validators []Validator) error {
for _, v := range validators {
if err := v(name); err != nil {
return fmt.Errorf("name validation failed: %w", err)
}
}
return nil
}
func notReserved() Validator {
return func(s string) error {
if s == ".." || s == "." {
return errors.New(". and .. are reserved names")
}
return nil
}
}
func notEmpty() Validator {
return func(s string) error {
if strings.TrimSpace(s) == "" {
return errors.New("must not be empty")
}
return nil
}
}
func doesNotContain(bad []string) Validator {
return func(s string) error {
for _, b := range bad {
if strings.Contains(s, b) {
return fmt.Errorf("must not contain %s", b)
}
}
return nil
}
}
func isShorterThan(maxLength int) Validator {
return func(s string) error {
if len(s) > maxLength {
return fmt.Errorf("must be shorter than %d", maxLength)
}
return nil
}
}
@@ -0,0 +1,258 @@
// Copyright 2018-2021 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 (
"context"
"net/http"
"path"
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/config"
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/errors"
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/net"
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/propfind"
"github.com/opencloud-eu/reva/v2/pkg/storagespace"
"github.com/opencloud-eu/reva/v2/pkg/utils"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
"github.com/opencloud-eu/reva/v2/pkg/rhttp/router"
)
// VersionsHandler handles version requests
type VersionsHandler struct {
}
func (h *VersionsHandler) init(c *config.Config) error {
return nil
}
// Handler handles requests
// versions can be listed with a PROPFIND to /remote.php/dav/meta/<fileid>/v
// a version is identified by a timestamp, eg. /remote.php/dav/meta/<fileid>/v/1561410426
func (h *VersionsHandler) Handler(s *svc, rid *provider.ResourceId) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
if rid == nil {
http.Error(w, "404 Not Found", http.StatusNotFound)
return
}
// baseURI is encoded as part of the response payload in href field
baseURI := path.Join(ctx.Value(net.CtxKeyBaseURI).(string), storagespace.FormatResourceID(rid))
ctx = context.WithValue(ctx, net.CtxKeyBaseURI, baseURI)
r = r.WithContext(ctx)
var key string
key, r.URL.Path = router.ShiftPath(r.URL.Path)
if r.Method == http.MethodOptions {
s.handleOptions(w, r)
return
}
if key == "" && r.Method == MethodPropfind {
h.doListVersions(w, r, s, rid)
return
}
if key != "" {
switch r.Method {
case MethodCopy:
// TODO(jfd) cs3api has no delete file version call
// TODO(jfd) restore version to given Destination, but cs3api has no destination
h.doRestore(w, r, s, rid, key)
return
case http.MethodHead:
log := appctx.GetLogger(ctx)
ref := &provider.Reference{
ResourceId: &provider.ResourceId{
StorageId: rid.StorageId,
SpaceId: rid.SpaceId,
OpaqueId: key,
},
Path: utils.MakeRelativePath(r.URL.Path),
}
s.handleHead(ctx, w, r, ref, *log)
return
case http.MethodGet:
log := appctx.GetLogger(ctx)
ref := &provider.Reference{
ResourceId: &provider.ResourceId{
StorageId: rid.StorageId,
SpaceId: rid.SpaceId,
OpaqueId: key,
},
Path: utils.MakeRelativePath(r.URL.Path),
}
s.handleGet(ctx, w, r, ref, "spaces", *log)
return
}
}
http.Error(w, "501 Forbidden", http.StatusNotImplemented)
})
}
func (h *VersionsHandler) doListVersions(w http.ResponseWriter, r *http.Request, s *svc, rid *provider.ResourceId) {
ctx, span := appctx.GetTracerProvider(r.Context()).Tracer(tracerName).Start(r.Context(), "listVersions")
defer span.End()
sublog := appctx.GetLogger(ctx).With().Interface("resourceid", rid).Logger()
pf, status, err := propfind.ReadPropfind(r.Body)
if err != nil {
sublog.Debug().Err(err).Msg("error reading propfind request")
w.WriteHeader(status)
return
}
client, err := s.gatewaySelector.Next()
if err != nil {
sublog.Error().Err(err).Msg("error selecting next gateway client")
w.WriteHeader(http.StatusInternalServerError)
return
}
ref := &provider.Reference{ResourceId: rid}
res, err := client.Stat(ctx, &provider.StatRequest{Ref: ref})
if err != nil {
sublog.Error().Err(err).Msg("error sending a grpc stat request")
w.WriteHeader(http.StatusInternalServerError)
return
}
if res.Status.Code != rpc.Code_CODE_OK {
if res.Status.Code == rpc.Code_CODE_PERMISSION_DENIED || res.Status.Code == rpc.Code_CODE_NOT_FOUND {
w.WriteHeader(http.StatusNotFound)
b, err := errors.Marshal(http.StatusNotFound, "Resource not found", "", "")
errors.HandleWebdavError(&sublog, w, b, err)
return
}
errors.HandleErrorStatus(&sublog, w, res.Status)
return
}
info := res.Info
lvRes, err := client.ListFileVersions(ctx, &provider.ListFileVersionsRequest{Ref: ref})
if err != nil {
sublog.Error().Err(err).Msg("error sending list container grpc request")
w.WriteHeader(http.StatusInternalServerError)
return
}
if lvRes.Status.Code != rpc.Code_CODE_OK {
if lvRes.Status.Code == rpc.Code_CODE_PERMISSION_DENIED {
w.WriteHeader(http.StatusForbidden)
b, err := errors.Marshal(http.StatusForbidden, "You have no permission to list file versions on this resource", "", "")
errors.HandleWebdavError(&sublog, w, b, err)
return
}
errors.HandleErrorStatus(&sublog, w, lvRes.Status)
return
}
versions := lvRes.GetVersions()
infos := make([]*provider.ResourceInfo, 0, len(versions)+1)
// add version dir . entry, derived from file info
infos = append(infos, &provider.ResourceInfo{
Type: provider.ResourceType_RESOURCE_TYPE_CONTAINER,
})
for i := range versions {
vi := &provider.ResourceInfo{
// TODO(jfd) we cannot access version content, this will be a problem when trying to fetch version thumbnails
// Opaque
Type: provider.ResourceType_RESOURCE_TYPE_FILE,
Id: &provider.ResourceId{
StorageId: "versions",
OpaqueId: info.Id.OpaqueId + "@" + versions[i].GetKey(),
},
// Checksum
Etag: versions[i].Etag,
// MimeType
Mtime: &types.Timestamp{
Seconds: versions[i].Mtime,
// TODO cs3apis FileVersion should use types.Timestamp instead of uint64
},
Path: path.Join("v", versions[i].Key),
// PermissionSet
Size: versions[i].Size,
Owner: info.Owner,
}
infos = append(infos, vi)
}
prefer := net.ParsePrefer(r.Header.Get("prefer"))
returnMinimal := prefer[net.HeaderPreferReturn] == "minimal"
propRes, err := propfind.MultistatusResponse(ctx, &pf, infos, s.c.PublicURL, "", nil, returnMinimal)
if err != nil {
sublog.Error().Err(err).Msg("error formatting propfind")
w.WriteHeader(http.StatusInternalServerError)
return
}
w.Header().Set(net.HeaderDav, "1, 3, extended-mkcol")
w.Header().Set(net.HeaderContentType, "application/xml; charset=utf-8")
w.Header().Set(net.HeaderVary, net.HeaderPrefer)
if returnMinimal {
w.Header().Set(net.HeaderPreferenceApplied, "return=minimal")
}
w.WriteHeader(http.StatusMultiStatus)
_, err = w.Write(propRes)
if err != nil {
sublog.Error().Err(err).Msg("error writing body")
return
}
}
func (h *VersionsHandler) doRestore(w http.ResponseWriter, r *http.Request, s *svc, rid *provider.ResourceId, key string) {
ctx, span := appctx.GetTracerProvider(r.Context()).Tracer(tracerName).Start(r.Context(), "restore")
defer span.End()
sublog := appctx.GetLogger(ctx).With().Interface("resourceid", rid).Str("key", key).Logger()
req := &provider.RestoreFileVersionRequest{
Ref: &provider.Reference{ResourceId: rid},
Key: key,
}
client, err := s.gatewaySelector.Next()
if err != nil {
sublog.Error().Err(err).Msg("error selecting next gateway client")
w.WriteHeader(http.StatusInternalServerError)
return
}
res, err := client.RestoreFileVersion(ctx, req)
if err != nil {
sublog.Error().Err(err).Msg("error sending a grpc restore version request")
w.WriteHeader(http.StatusInternalServerError)
return
}
if res.Status.Code != rpc.Code_CODE_OK {
if res.Status.Code == rpc.Code_CODE_PERMISSION_DENIED {
w.WriteHeader(http.StatusForbidden)
b, err := errors.Marshal(http.StatusForbidden, "You have no permission to restore versions on this resource", "", "")
errors.HandleWebdavError(&sublog, w, b, err)
return
}
errors.HandleErrorStatus(&sublog, w, res.Status)
return
}
w.WriteHeader(http.StatusNoContent)
}
@@ -0,0 +1,120 @@
// Copyright 2018-2021 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 (
"fmt"
"net/http"
"path"
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/errors"
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/propfind"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
)
// 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"
)
// WebDavHandler implements a dav endpoint
type WebDavHandler struct {
namespace string
useLoggedInUserNS bool
}
func (h *WebDavHandler) init(ns string, useLoggedInUserNS bool) error {
h.namespace = path.Join("/", ns)
h.useLoggedInUserNS = useLoggedInUserNS
return nil
}
// Handler handles requests
func (h *WebDavHandler) Handler(s *svc) http.Handler {
config := s.Config()
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ns, newPath, err := s.ApplyLayout(r.Context(), h.namespace, h.useLoggedInUserNS, r.URL.Path)
if err != nil {
w.WriteHeader(http.StatusNotFound)
b, err := errors.Marshal(http.StatusNotFound, fmt.Sprintf("could not get storage for %s", r.URL.Path), "", "")
errors.HandleWebdavError(appctx.GetLogger(r.Context()), w, b, err)
return
}
r.URL.Path = newPath
// TODO initialize status with http.StatusBadRequest
// TODO initialize err with errors.ErrUnsupportedMethod
var status int // status 0 means the handler already sent the response
switch r.Method {
case MethodPropfind:
p := propfind.NewHandler(config.PublicURL, s.gatewaySelector, config)
p.HandlePathPropfind(w, r, ns)
case MethodLock:
status, err = s.handleLock(w, r, ns)
case MethodUnlock:
status, err = s.handleUnlock(w, r, ns)
case MethodProppatch:
status, err = s.handlePathProppatch(w, r, ns)
case MethodMkcol:
status, err = 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.handlePathGet(w, r, ns)
case http.MethodPut:
s.handlePathPut(w, r, ns)
case http.MethodPost:
s.handlePathTusPost(w, r, ns)
case http.MethodOptions:
s.handleOptions(w, r)
case http.MethodHead:
s.handlePathHead(w, r, ns)
case http.MethodDelete:
status, err = s.handlePathDelete(w, r, ns)
default:
w.WriteHeader(http.StatusNotFound)
}
if status != 0 { // 0 means the handler already sent the response
w.WriteHeader(status)
if status != http.StatusNoContent {
var b []byte
if b, err = errors.Marshal(status, err.Error(), "", ""); err == nil {
_, err = w.Write(b)
}
}
}
if err != nil {
appctx.GetLogger(r.Context()).Error().Err(err).Msg(err.Error())
}
})
}
@@ -0,0 +1,73 @@
// Copyright 2018-2021 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 ocs
import (
"context"
"net/http"
"net/http/httptest"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
"google.golang.org/grpc/metadata"
)
func (s *svc) cacheWarmup(w http.ResponseWriter, r *http.Request) {
if s.warmupCacheTracker != nil {
u, ok1 := ctxpkg.ContextGetUser(r.Context())
tkn, ok2 := ctxpkg.ContextGetToken(r.Context())
if !ok1 || !ok2 {
return
}
log := appctx.GetLogger(r.Context())
// We make a copy of the context because the original one comes with its cancel channel,
// so once the initial request is finished, this ctx gets cancelled as well.
// And in most of the cases, the warmup takes a longer amount of time to complete than the original request.
// TODO: Check if we can come up with a better solution, eg, https://stackoverflow.com/a/54132324
ctx := context.Background()
ctx = appctx.WithLogger(ctx, log)
ctx = ctxpkg.ContextSetUser(ctx, u)
ctx = ctxpkg.ContextSetToken(ctx, tkn)
ctx = metadata.AppendToOutgoingContext(ctx, ctxpkg.TokenHeader, tkn)
req, _ := http.NewRequest("GET", "", nil)
req = req.WithContext(ctx)
req.URL = r.URL
id := u.Id.OpaqueId
if _, err := s.warmupCacheTracker.Get(id); err != nil {
p := httptest.NewRecorder()
_ = s.warmupCacheTracker.Set(id, true)
log.Info().Msgf("cache warmup getting created shares for user %s", id)
req.URL.Path = "/v1.php/apps/files_sharing/api/v1/shares"
s.router.ServeHTTP(p, req)
log.Info().Msgf("cache warmup getting received shares for user %s", id)
req.URL.Path = "/v1.php/apps/files_sharing/api/v1/shares"
q := req.URL.Query()
q.Set("shared_with_me", "true")
q.Set("state", "all")
req.URL.RawQuery = q.Encode()
s.router.ServeHTTP(p, req)
}
}
}
@@ -0,0 +1,81 @@
// Copyright 2018-2021 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 config
import (
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocs/data"
"github.com/opencloud-eu/reva/v2/pkg/owncloud/ocs"
"github.com/opencloud-eu/reva/v2/pkg/sharedconf"
"github.com/opencloud-eu/reva/v2/pkg/storage/cache"
)
// Config holds the config options that need to be passed down to all ocs handlers
type Config struct {
Prefix string `mapstructure:"prefix"`
Config data.ConfigData `mapstructure:"config"`
Capabilities ocs.CapabilitiesData `mapstructure:"capabilities"`
GatewaySvc string `mapstructure:"gatewaysvc"`
StorageregistrySvc string `mapstructure:"storage_registry_svc"`
DefaultUploadProtocol string `mapstructure:"default_upload_protocol"`
UserAgentChunkingMap map[string]string `mapstructure:"user_agent_chunking_map"`
SharePrefix string `mapstructure:"share_prefix"`
HomeNamespace string `mapstructure:"home_namespace"`
AdditionalInfoAttribute string `mapstructure:"additional_info_attribute"`
CacheWarmupDriver string `mapstructure:"cache_warmup_driver"`
CacheWarmupDrivers map[string]map[string]interface{} `mapstructure:"cache_warmup_drivers"`
StatCacheConfig cache.Config `mapstructure:"stat_cache_config"`
UserIdentifierCacheTTL int `mapstructure:"user_identifier_cache_ttl"`
MachineAuthAPIKey string `mapstructure:"machine_auth_apikey"`
SkipUpdatingExistingSharesMountpoints bool `mapstructure:"skip_updating_existing_shares_mountpoint"`
EnableDenials bool `mapstructure:"enable_denials"`
OCMMountPoint string `mapstructure:"ocm_mount_point"`
ListOCMShares bool `mapstructure:"list_ocm_shares"`
Notifications map[string]interface{} `mapstructure:"notifications"`
IncludeOCMSharees bool `mapstructure:"include_ocm_sharees"`
ShowEmailInResults bool `mapstructure:"show_email_in_results"`
}
// Init sets sane defaults
func (c *Config) Init() {
if c.Prefix == "" {
c.Prefix = "ocs"
}
if c.SharePrefix == "" {
c.SharePrefix = "/Shares"
}
if c.DefaultUploadProtocol == "" {
c.DefaultUploadProtocol = "tus"
}
if c.HomeNamespace == "" {
c.HomeNamespace = "/users/{{.Id.OpaqueId}}"
}
if c.AdditionalInfoAttribute == "" {
c.AdditionalInfoAttribute = "{{.Mail}}"
}
if c.UserIdentifierCacheTTL == 0 {
c.UserIdentifierCacheTTL = 60
}
c.GatewaySvc = sharedconf.GetGatewaySVC(c.GatewaySvc)
}
@@ -0,0 +1,28 @@
// Copyright 2018-2021 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 data
// ConfigData holds basic config
type ConfigData struct {
Version string `json:"version" xml:"version"`
Website string `json:"website" xml:"website"`
Host string `json:"host" xml:"host"`
Contact string `json:"contact" xml:"contact"`
SSL string `json:"ssl" xml:"ssl"`
}
@@ -0,0 +1,201 @@
// Copyright 2018-2021 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 sharees
import (
"net/http"
"strings"
grouppb "github.com/cs3org/go-cs3apis/cs3/identity/group/v1beta1"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
invitepb "github.com/cs3org/go-cs3apis/cs3/ocm/invite/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
"github.com/opencloud-eu/reva/v2/pkg/conversions"
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocs/config"
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocs/response"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/opencloud-eu/reva/v2/pkg/storage/utils/templates"
)
// Handler implements the ownCloud sharing API
type Handler struct {
gatewayAddr string
additionalInfoAttribute string
includeOCMSharees bool
showUserEmailInResults bool
}
// Init initializes this and any contained handlers
func (h *Handler) Init(c *config.Config) {
h.gatewayAddr = c.GatewaySvc
h.additionalInfoAttribute = c.AdditionalInfoAttribute
h.includeOCMSharees = c.IncludeOCMSharees
h.showUserEmailInResults = c.ShowEmailInResults
}
// FindSharees implements the /apps/files_sharing/api/v1/sharees endpoint
func (h *Handler) FindSharees(w http.ResponseWriter, r *http.Request) {
log := appctx.GetLogger(r.Context())
term := r.URL.Query().Get("search")
if term == "" {
response.WriteOCSError(w, r, response.MetaBadRequest.StatusCode, "search must not be empty", nil)
return
}
gwc, err := pool.GetGatewayServiceClient(h.gatewayAddr)
if err != nil {
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error getting gateway grpc client", err)
return
}
usersRes, err := gwc.FindUsers(r.Context(), &userpb.FindUsersRequest{Filter: term, SkipFetchingUserGroups: true})
if err != nil {
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error searching users", err)
return
}
log.Debug().Int("count", len(usersRes.GetUsers())).Str("search", term).Msg("users found")
userMatches := make([]*conversions.MatchData, 0, len(usersRes.GetUsers()))
exactUserMatches := make([]*conversions.MatchData, 0)
for _, user := range usersRes.GetUsers() {
match := h.userAsMatch(user)
log.Debug().Interface("user", user).Interface("match", match).Msg("mapped")
if h.isExactMatch(match, term) {
exactUserMatches = append(exactUserMatches, match)
} else {
userMatches = append(userMatches, match)
}
}
if h.includeOCMSharees {
remoteUsersRes, err := gwc.FindAcceptedUsers(r.Context(), &invitepb.FindAcceptedUsersRequest{Filter: term})
if err != nil {
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error searching remote users", err)
return
}
if remoteUsersRes.Status.Code != rpc.Code_CODE_OK {
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error searching remote users", nil)
return
}
for _, user := range remoteUsersRes.GetAcceptedUsers() {
match := h.userAsMatch(user)
log.Debug().Interface("user", user).Interface("match", match).Msg("mapped")
if h.isExactMatch(match, term) {
exactUserMatches = append(exactUserMatches, match)
} else {
userMatches = append(userMatches, match)
}
}
}
groupsRes, err := gwc.FindGroups(r.Context(), &grouppb.FindGroupsRequest{Filter: term, SkipFetchingMembers: true})
if err != nil {
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error searching groups", err)
return
}
log.Debug().Int("count", len(groupsRes.GetGroups())).Str("search", term).Msg("groups found")
groupMatches := make([]*conversions.MatchData, 0, len(groupsRes.GetGroups()))
exactGroupMatches := make([]*conversions.MatchData, 0)
for _, g := range groupsRes.GetGroups() {
match := h.groupAsMatch(g)
log.Debug().Interface("group", g).Interface("match", match).Msg("mapped")
if h.isExactMatch(match, term) {
exactGroupMatches = append(exactGroupMatches, match)
} else {
groupMatches = append(groupMatches, match)
}
}
if !h.showUserEmailInResults {
for _, m := range userMatches {
m.Value.ShareWithAdditionalInfo = m.Value.ShareWith
}
for _, m := range exactUserMatches {
m.Value.ShareWithAdditionalInfo = m.Value.ShareWith
}
for _, m := range groupMatches {
m.Value.ShareWithAdditionalInfo = m.Value.ShareWith
}
for _, m := range exactGroupMatches {
m.Value.ShareWithAdditionalInfo = m.Value.ShareWith
}
}
response.WriteOCSSuccess(w, r, &conversions.ShareeData{
Exact: &conversions.ExactMatchesData{
Users: exactUserMatches,
Groups: exactGroupMatches,
Remotes: []*conversions.MatchData{},
},
Users: userMatches,
Groups: groupMatches,
Remotes: []*conversions.MatchData{},
})
}
func (h *Handler) userAsMatch(u *userpb.User) *conversions.MatchData {
data := &conversions.MatchValueData{
ShareType: int(conversions.ShareTypeUser),
// api compatibility with oc10: mark guest users in share invite dialogue
UserType: 0,
// api compatibility with oc10: always use the username
ShareWith: u.Username,
ShareWithAdditionalInfo: h.getAdditionalInfoAttribute(u),
}
switch u.Id.Type {
case userpb.UserType_USER_TYPE_GUEST, userpb.UserType_USER_TYPE_LIGHTWEIGHT:
data.UserType = 1
case userpb.UserType_USER_TYPE_FEDERATED:
data.ShareType = int(conversions.ShareTypeFederatedCloudShare)
data.ShareWith = u.Id.OpaqueId
data.ShareWithProvider = u.Id.Idp
}
return &conversions.MatchData{
Label: u.DisplayName,
Value: data,
}
}
func (h *Handler) groupAsMatch(g *grouppb.Group) *conversions.MatchData {
return &conversions.MatchData{
Label: g.DisplayName,
Value: &conversions.MatchValueData{
ShareType: int(conversions.ShareTypeGroup),
ShareWith: g.GroupName,
ShareWithAdditionalInfo: g.Mail,
},
}
}
func (h *Handler) getAdditionalInfoAttribute(u *userpb.User) string {
return templates.WithUser(u, h.additionalInfoAttribute)
}
func (h *Handler) isExactMatch(match *conversions.MatchData, term string) bool {
if match == nil || match.Value == nil {
return false
}
return strings.EqualFold(match.Value.ShareWith, term) || strings.EqualFold(match.Value.ShareWithAdditionalInfo, term) ||
strings.EqualFold(match.Label, term)
}
@@ -0,0 +1,169 @@
// Copyright 2018-2022 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 sharees
import (
"context"
"errors"
"fmt"
"net/http"
"path"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
link "github.com/cs3org/go-cs3apis/cs3/sharing/link/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocdav/spacelookup"
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocs/response"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
"github.com/opencloud-eu/reva/v2/pkg/conversions"
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/opencloud-eu/reva/v2/pkg/storagespace"
"github.com/opencloud-eu/reva/v2/pkg/utils"
"google.golang.org/grpc/metadata"
)
// TokenInfo handles http requests regarding tokens
func (h *Handler) TokenInfo(protected bool) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
log := appctx.GetLogger(r.Context())
tkn := path.Base(r.URL.Path)
_, pw, _ := r.BasicAuth()
c, err := pool.GetGatewayServiceClient(h.gatewayAddr)
if err != nil {
// endpoint public - don't exponse information
log.Error().Err(err).Msg("error getting gateway client")
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "", nil)
return
}
t, err := handleGetToken(r.Context(), tkn, pw, c, protected)
if err != nil {
// endpoint public - don't exponse information
log.Error().Err(err).Msg("error while handling GET TokenInfo")
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "", nil)
return
}
response.WriteOCSSuccess(w, r, t)
}
}
func handleGetToken(ctx context.Context, tkn string, pw string, c gateway.GatewayAPIClient, protected bool) (conversions.TokenInfo, error) {
user, token, passwordProtected, err := getInfoForToken(tkn, pw, c)
if err != nil {
return conversions.TokenInfo{}, err
}
t, err := buildTokenInfo(user, tkn, token, passwordProtected, c)
if err != nil {
return t, err
}
if protected && !t.PasswordProtected {
space, status, err := spacelookup.LookUpStorageSpaceByID(ctx, c, storagespace.FormatResourceID(&provider.ResourceId{StorageId: t.StorageID, SpaceId: t.SpaceID, OpaqueId: t.OpaqueID}))
// add info only if user is able to stat
if err == nil && status.Code == rpc.Code_CODE_OK {
t.SpacePath = utils.ReadPlainFromOpaque(space.Opaque, "path")
t.SpaceAlias = utils.ReadPlainFromOpaque(space.Opaque, "spaceAlias")
t.SpaceURL = path.Join(t.SpaceAlias, t.OpaqueID, t.Path)
t.SpaceType = space.SpaceType
}
}
return t, nil
}
func buildTokenInfo(owner *user.User, tkn string, token string, passProtected bool, c gateway.GatewayAPIClient) (conversions.TokenInfo, error) {
t := conversions.TokenInfo{Token: tkn, LinkURL: "/s/" + tkn}
if passProtected {
t.PasswordProtected = true
return t, nil
}
ctx := ctxpkg.ContextSetToken(context.TODO(), token)
ctx = ctxpkg.ContextSetUser(ctx, owner)
ctx = metadata.AppendToOutgoingContext(ctx, ctxpkg.TokenHeader, token)
sRes, err := getPublicShare(ctx, c, tkn)
if err != nil || sRes.Status.Code != rpc.Code_CODE_OK {
return t, fmt.Errorf("can't stat resource. %+v %s", sRes, err)
}
t.ID = storagespace.FormatResourceID(sRes.Share.GetResourceId())
t.StorageID = sRes.Share.ResourceId.GetStorageId()
t.SpaceID = sRes.Share.ResourceId.GetSpaceId()
t.OpaqueID = sRes.Share.ResourceId.GetOpaqueId()
role := conversions.RoleFromResourcePermissions(sRes.Share.Permissions.GetPermissions(), true)
t.Aliaslink = role.OCSPermissions() == 0
return t, nil
}
func getInfoForToken(tkn string, pw string, c gateway.GatewayAPIClient) (owner *user.User, token string, passwordProtected bool, err error) {
ctx := context.Background()
res, err := handleBasicAuth(ctx, c, tkn, pw)
if err != nil {
return
}
switch res.Status.Code {
case rpc.Code_CODE_OK:
// nothing to do
case rpc.Code_CODE_PERMISSION_DENIED:
if res.Status.Message != "wrong password" {
err = errors.New("not found")
return
}
passwordProtected = true
return
default:
err = fmt.Errorf("authentication returned unsupported status code '%d'", res.Status.Code)
return
}
return res.User, res.Token, false, nil
}
func handleBasicAuth(ctx context.Context, c gateway.GatewayAPIClient, token, pw string) (*gateway.AuthenticateResponse, error) {
authenticateRequest := gateway.AuthenticateRequest{
Type: "publicshares",
ClientId: token,
ClientSecret: "password|" + pw,
}
return c.Authenticate(ctx, &authenticateRequest)
}
func getPublicShare(ctx context.Context, client gateway.GatewayAPIClient, token string) (*link.GetPublicShareResponse, error) {
return client.GetPublicShare(ctx, &link.GetPublicShareRequest{
Ref: &link.PublicShareReference{
Spec: &link.PublicShareReference_Token{
Token: token,
},
}})
}
@@ -0,0 +1,100 @@
// Copyright 2018-2021 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 shares
import (
"net/http"
grouppb "github.com/cs3org/go-cs3apis/cs3/identity/group/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/opencloud-eu/reva/v2/pkg/conversions"
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocs/response"
)
func (h *Handler) createGroupShare(w http.ResponseWriter, r *http.Request, statInfo *provider.ResourceInfo, role *conversions.Role, roleVal []byte) (*collaboration.Share, *ocsError) {
ctx := r.Context()
c, err := h.getClient()
if err != nil {
return nil, &ocsError{
Code: response.MetaServerError.StatusCode,
Message: "error getting grpc gateway client",
Error: err,
}
}
shareWith := r.FormValue("shareWith")
if shareWith == "" {
return nil, &ocsError{
Code: response.MetaBadRequest.StatusCode,
Message: "missing shareWith",
}
}
groupRes, err := c.GetGroupByClaim(ctx, &grouppb.GetGroupByClaimRequest{
Claim: "group_name",
Value: shareWith,
SkipFetchingMembers: true,
})
if err != nil {
return nil, &ocsError{
Code: response.MetaServerError.StatusCode,
Message: "error searching recipient",
Error: err,
}
}
if groupRes.Status.Code != rpc.Code_CODE_OK {
return nil, &ocsError{
Code: response.MetaNotFound.StatusCode,
Message: "group not found",
Error: err,
}
}
createShareReq := &collaboration.CreateShareRequest{
Opaque: &types.Opaque{
Map: map[string]*types.OpaqueEntry{
"role": {
Decoder: "json",
Value: roleVal,
},
},
},
ResourceInfo: statInfo,
Grant: &collaboration.ShareGrant{
Grantee: &provider.Grantee{
Type: provider.GranteeType_GRANTEE_TYPE_GROUP,
Id: &provider.Grantee_GroupId{GroupId: groupRes.Group.GetId()},
},
Permissions: &collaboration.SharePermissions{
Permissions: role.CS3ResourcePermissions(),
},
},
}
share, ocsErr := h.createCs3Share(ctx, w, r, c, createShareReq)
if ocsErr != nil {
return nil, ocsErr
}
return share, nil
}
@@ -0,0 +1,377 @@
// Copyright 2018-2021 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 shares
import (
"context"
"fmt"
"net/http"
"path"
"strconv"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1"
ocmv1beta1 "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/go-chi/chi/v5"
"github.com/pkg/errors"
"google.golang.org/protobuf/types/known/fieldmaskpb"
"github.com/opencloud-eu/reva/v2/internal/grpc/services/usershareprovider"
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocs/response"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
"github.com/opencloud-eu/reva/v2/pkg/conversions"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
)
const (
// shareidkey is the key user to obtain the id of the share to update. It is present in the request URL.
shareidkey string = "shareid"
)
// AcceptReceivedShare handles Post Requests on /apps/files_sharing/api/v1/shares/{shareid}
func (h *Handler) AcceptReceivedShare(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
shareID := chi.URLParam(r, shareidkey)
if h.isFederatedReceivedShare(r, shareID) {
h.updateReceivedFederatedShare(w, r, shareID, false)
return
}
client, err := h.getClient()
if err != nil {
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error getting grpc gateway client", err)
return
}
receivedShare, ocsResponse := getReceivedShareFromID(ctx, client, shareID)
if ocsResponse != nil {
response.WriteOCSResponse(w, r, *ocsResponse, nil)
return
}
sharedResource, ocsResponse := getSharedResource(ctx, client, receivedShare.Share.ResourceId)
if ocsResponse != nil {
response.WriteOCSResponse(w, r, *ocsResponse, nil)
return
}
mount, unmountedShares, err := usershareprovider.GetMountpointAndUnmountedShares(
ctx,
client,
sharedResource.GetInfo().GetId(),
sharedResource.GetInfo().GetName(),
nil,
)
if err != nil {
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "could not determine mountpoint", err)
return
}
// first update the requested share
receivedShare.State = collaboration.ShareState_SHARE_STATE_ACCEPTED
// we need to add a path to the share
receivedShare.MountPoint = &provider.Reference{
Path: mount,
}
updateMask := &fieldmaskpb.FieldMask{Paths: []string{"state", "mount_point"}}
data, meta, err := h.updateReceivedShare(r.Context(), receivedShare, updateMask)
if err != nil {
// we log an error for affected shares, for the actual share we return an error
response.WriteOCSData(w, r, meta, data, err)
return
}
response.WriteOCSSuccess(w, r, []*conversions.ShareData{data})
// then update other unmounted shares to the same resource
for _, rs := range unmountedShares {
if rs.GetShare().GetId().GetOpaqueId() == shareID {
// we already updated this one
continue
}
rs.State = collaboration.ShareState_SHARE_STATE_ACCEPTED
// set the same mountpoint as for the requested received share
rs.MountPoint = &provider.Reference{
Path: mount,
}
_, _, err := h.updateReceivedShare(r.Context(), rs, updateMask)
if err != nil {
// we log an error for affected shares, the actual share was successful
appctx.GetLogger(ctx).Error().Err(err).Str("received_share", shareID).Str("affected_share", rs.GetShare().GetId().GetOpaqueId()).Msg("could not update affected received share")
}
}
}
// RejectReceivedShare handles DELETE Requests on /apps/files_sharing/api/v1/shares/{shareid}
func (h *Handler) RejectReceivedShare(w http.ResponseWriter, r *http.Request) {
shareID := chi.URLParam(r, "shareid")
if h.isFederatedReceivedShare(r, shareID) {
h.updateReceivedFederatedShare(w, r, shareID, true)
return
}
// we need to add a path to the share
receivedShare := &collaboration.ReceivedShare{
Share: &collaboration.Share{
Id: &collaboration.ShareId{OpaqueId: shareID},
},
State: collaboration.ShareState_SHARE_STATE_REJECTED,
}
updateMask := &fieldmaskpb.FieldMask{Paths: []string{"state"}}
data, meta, err := h.updateReceivedShare(r.Context(), receivedShare, updateMask)
if err != nil {
response.WriteOCSData(w, r, meta, nil, err)
}
response.WriteOCSSuccess(w, r, []*conversions.ShareData{data})
}
func (h *Handler) UpdateReceivedShare(w http.ResponseWriter, r *http.Request) {
shareID := chi.URLParam(r, "shareid")
hideFlag, _ := strconv.ParseBool(r.URL.Query().Get("hidden"))
// unfortunately we need to get the share first to read the state
client, err := h.getClient()
if err != nil {
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error getting grpc gateway client", err)
return
}
// we need to add a path to the share
receivedShare := &collaboration.ReceivedShare{
Share: &collaboration.Share{
Id: &collaboration.ShareId{OpaqueId: shareID},
},
Hidden: hideFlag,
}
updateMask := &fieldmaskpb.FieldMask{Paths: []string{"state", "hidden"}}
rs, _ := getReceivedShareFromID(r.Context(), client, shareID)
if rs != nil && rs.Share != nil {
receivedShare.State = rs.State
}
data, meta, err := h.updateReceivedShare(r.Context(), receivedShare, updateMask)
if err != nil {
response.WriteOCSData(w, r, meta, nil, err)
}
response.WriteOCSSuccess(w, r, []*conversions.ShareData{data})
}
func (h *Handler) updateReceivedShare(ctx context.Context, receivedShare *collaboration.ReceivedShare, fieldMask *fieldmaskpb.FieldMask) (*conversions.ShareData, response.Meta, error) {
logger := appctx.GetLogger(ctx)
updateShareRequest := &collaboration.UpdateReceivedShareRequest{
Share: receivedShare,
UpdateMask: fieldMask,
}
client, err := h.getClient()
if err != nil {
return nil, response.MetaServerError, errors.Wrap(err, "error getting grpc gateway client")
}
shareRes, err := client.UpdateReceivedShare(ctx, updateShareRequest)
if err != nil {
return nil, response.MetaServerError, errors.Wrap(err, "grpc update received share request failed")
}
if shareRes.Status.Code != rpc.Code_CODE_OK {
if shareRes.Status.Code == rpc.Code_CODE_NOT_FOUND {
return nil, response.MetaNotFound, errors.New(shareRes.Status.Message)
}
return nil, response.MetaServerError, errors.Errorf("grpc update received share request failed: code: %d, message: %s", shareRes.Status.Code, shareRes.Status.Message)
}
rs := shareRes.GetShare()
info, status, err := h.getResourceInfoByID(ctx, client, rs.Share.ResourceId)
if err != nil || status.Code != rpc.Code_CODE_OK {
h.logProblems(logger, status, err, "could not stat, skipping")
return nil, response.MetaServerError, errors.Errorf("grpc get resource info failed: code: %d, message: %s", status.Code, status.Message)
}
data := conversions.CS3Share2ShareData(ctx, rs.Share)
data.State = mapState(rs.GetState())
data.Hidden = rs.GetHidden()
h.addFileInfo(ctx, data, info)
h.mapUserIds(ctx, client, data)
if data.State == ocsStateAccepted {
// Needed because received shares can be jailed in a folder in the users home
data.Path = path.Join(h.sharePrefix, path.Base(info.Path))
}
return data, response.MetaOK, nil
}
func (h *Handler) updateReceivedFederatedShare(w http.ResponseWriter, r *http.Request, shareID string, rejectShare bool) {
ctx := r.Context()
client, err := h.getClient()
if err != nil {
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error getting grpc gateway client", err)
return
}
share, err := client.GetReceivedOCMShare(ctx, &ocmv1beta1.GetReceivedOCMShareRequest{
Ref: &ocmv1beta1.ShareReference{
Spec: &ocmv1beta1.ShareReference_Id{
Id: &ocmv1beta1.ShareId{
OpaqueId: shareID,
},
},
},
})
if err != nil {
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "grpc update received share request failed", err)
return
}
if share.Status.Code != rpc.Code_CODE_OK {
if share.Status.Code == rpc.Code_CODE_NOT_FOUND {
response.WriteOCSError(w, r, response.MetaNotFound.StatusCode, "not found", nil)
return
}
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "grpc update received share request failed", errors.Errorf("code: %d, message: %s", share.Status.Code, share.Status.Message))
return
}
req := &ocmv1beta1.UpdateReceivedOCMShareRequest{
Share: &ocmv1beta1.ReceivedShare{
Id: &ocmv1beta1.ShareId{
OpaqueId: shareID,
},
},
UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"state"}},
}
if rejectShare {
req.Share.State = ocmv1beta1.ShareState_SHARE_STATE_REJECTED
} else {
req.Share.State = ocmv1beta1.ShareState_SHARE_STATE_ACCEPTED
}
updateRes, err := client.UpdateReceivedOCMShare(ctx, req)
if err != nil {
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "grpc update received share request failed", err)
return
}
if updateRes.Status.Code != rpc.Code_CODE_OK {
if updateRes.Status.Code == rpc.Code_CODE_NOT_FOUND {
response.WriteOCSError(w, r, response.MetaNotFound.StatusCode, "not found", nil)
return
}
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "grpc update received share request failed", errors.Errorf("code: %d, message: %s", updateRes.Status.Code, updateRes.Status.Message))
return
}
data, err := conversions.ReceivedOCMShare2ShareData(share.Share, h.ocmLocalMount(share.Share))
if err != nil {
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "grpc update received share request failed", err)
return
}
h.mapUserIdsReceivedFederatedShare(ctx, client, data)
data.State = mapOCMState(req.Share.State)
response.WriteOCSSuccess(w, r, []*conversions.ShareData{data})
}
// getReceivedShareFromID uses a client to the gateway to fetch a share based on its ID.
func getReceivedShareFromID(ctx context.Context, client gateway.GatewayAPIClient, shareID string) (*collaboration.ReceivedShare, *response.Response) {
s, err := client.GetReceivedShare(ctx, &collaboration.GetReceivedShareRequest{
Ref: &collaboration.ShareReference{
Spec: &collaboration.ShareReference_Id{
Id: &collaboration.ShareId{
OpaqueId: shareID,
}},
},
})
if err != nil {
e := errors.Wrap(err, fmt.Sprintf("could not get share with ID: `%s`", shareID))
return nil, arbitraryOcsResponse(response.MetaServerError.StatusCode, e.Error())
}
if s.Status.Code != rpc.Code_CODE_OK {
if s.Status.Code == rpc.Code_CODE_NOT_FOUND {
e := fmt.Errorf("share not found")
return nil, arbitraryOcsResponse(response.MetaNotFound.StatusCode, e.Error())
}
e := fmt.Errorf("invalid share: %s", s.GetStatus().GetMessage())
return nil, arbitraryOcsResponse(response.MetaBadRequest.StatusCode, e.Error())
}
return s.Share, nil
}
// getSharedResource attempts to get a shared resource from the storage from the resource reference.
func getSharedResource(ctx context.Context, client gateway.GatewayAPIClient, resID *provider.ResourceId) (*provider.StatResponse, *response.Response) {
res, err := client.Stat(ctx, &provider.StatRequest{
Ref: &provider.Reference{
ResourceId: resID,
},
})
if err != nil {
return nil, arbitraryOcsResponse(response.MetaServerError.StatusCode, "could not get reference")
}
if res.Status.Code != rpc.Code_CODE_OK {
if res.Status.Code == rpc.Code_CODE_NOT_FOUND {
return nil, arbitraryOcsResponse(response.MetaNotFound.StatusCode, "not found")
}
return nil, arbitraryOcsResponse(response.MetaServerError.StatusCode, res.GetStatus().GetMessage())
}
return res, nil
}
// listReceivedShares list all received shares for the current user.
func listReceivedShares(ctx context.Context, client gateway.GatewayAPIClient) ([]*collaboration.ReceivedShare, error) {
res, err := client.ListReceivedShares(ctx, &collaboration.ListReceivedSharesRequest{})
if err != nil {
return nil, errtypes.InternalError("grpc list received shares request failed")
}
if err := errtypes.NewErrtypeFromStatus(res.Status); err != nil {
return nil, err
}
return res.Shares, nil
}
// arbitraryOcsResponse abstracts the boilerplate that is creating a response.Response struct.
func arbitraryOcsResponse(statusCode int, message string) *response.Response {
r := response.Response{
OCS: &response.Payload{
XMLName: struct{}{},
Meta: response.Meta{},
Data: nil,
},
}
r.OCS.Meta.StatusCode = statusCode
r.OCS.Meta.Message = message
return &r
}
@@ -0,0 +1,736 @@
// Copyright 2018-2021 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 shares
import (
"context"
"encoding/json"
"fmt"
"net/http"
"strconv"
userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
link "github.com/cs3org/go-cs3apis/cs3/sharing/link/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/huandu/xstrings"
"github.com/opencloud-eu/reva/v2/pkg/conversions"
"github.com/opencloud-eu/reva/v2/pkg/permission"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/opencloud-eu/reva/v2/pkg/utils"
"github.com/rs/zerolog/log"
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocs/response"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
"github.com/opencloud-eu/reva/v2/pkg/publicshare"
"github.com/pkg/errors"
)
var _defaultPublicLinkPermission = 1
func (h *Handler) createPublicLinkShare(w http.ResponseWriter, r *http.Request, statInfo *provider.ResourceInfo) (*link.PublicShare, *ocsError) {
ctx := r.Context()
log := appctx.GetLogger(ctx)
c, err := h.getClient()
if err != nil {
return nil, &ocsError{
Code: response.MetaServerError.StatusCode,
Message: "error getting grpc gateway client",
Error: err,
}
}
permKey, err := permKeyFromRequest(r, h)
if err != nil {
return nil, &ocsError{
Code: response.MetaBadRequest.StatusCode,
Message: "Could not read permission from request",
Error: err,
}
}
// NOTE: one is allowed to create an internal link without the `Publink.Write` permission
if permKey != nil && *permKey != 0 {
ok, err := utils.CheckPermission(ctx, permission.WritePublicLink, c)
if err != nil {
return nil, &ocsError{
Code: response.MetaServerError.StatusCode,
Message: "failed to check user permission",
Error: err,
}
}
if !ok {
return nil, &ocsError{
Code: response.MetaForbidden.StatusCode,
Message: "user is not allowed to create a public link",
}
}
}
err = r.ParseForm()
if err != nil {
return nil, &ocsError{
Code: response.MetaBadRequest.StatusCode,
Message: "Could not parse form from request",
Error: err,
}
}
// check if a quicklink should be created
quick, _ := strconv.ParseBool(r.FormValue("quicklink")) // no need to check the error - defaults to zero value!
if quick {
f := []*link.ListPublicSharesRequest_Filter{publicshare.ResourceIDFilter(statInfo.Id)}
req := link.ListPublicSharesRequest{Filters: f}
res, err := c.ListPublicShares(ctx, &req)
if err != nil {
return nil, &ocsError{
Code: response.MetaServerError.StatusCode,
Message: "could not list public links",
Error: err,
}
}
if res.Status.Code != rpc.Code_CODE_OK {
return nil, &ocsError{
Code: int(res.Status.GetCode()),
Message: "could not list public links",
}
}
for _, l := range res.GetShare() {
if l.Quicklink {
return l, nil
}
}
}
// default perms: read-only
// TODO: the default might change depending on allowed permissions and configs
if permKey == nil {
permKey = &_defaultPublicLinkPermission
}
permissions, err := ocPublicPermToCs3(permKey)
if err != nil {
return nil, &ocsError{
Code: response.MetaBadRequest.StatusCode,
Message: "Could not create permission from permission key",
Error: err,
}
}
password := r.FormValue("password")
if h.enforcePassword(permKey) && len(password) == 0 {
return nil, &ocsError{
Code: response.MetaBadRequest.StatusCode,
Message: "missing required password",
Error: errors.New("missing required password"),
}
}
if len(password) > 0 {
if err := h.passwordValidator.Validate(password); err != nil {
return nil, &ocsError{
Code: response.MetaBadRequest.StatusCode,
Message: xstrings.FirstRuneToUpper(err.Error()),
Error: fmt.Errorf("password validation failed: %w", err),
}
}
}
if statInfo != nil && statInfo.Type == provider.ResourceType_RESOURCE_TYPE_FILE {
// Single file shares should never have delete or create permissions
role := conversions.RoleFromResourcePermissions(permissions, true)
p := role.OCSPermissions()
p &^= conversions.PermissionCreate
p &^= conversions.PermissionDelete
permissions = conversions.RoleFromOCSPermissions(p, statInfo).CS3ResourcePermissions()
}
if !sufficientPermissions(statInfo.PermissionSet, permissions, true) {
response.WriteOCSError(w, r, http.StatusForbidden, "no share permission", nil)
return nil, &ocsError{
Code: http.StatusForbidden,
Message: "Cannot set the requested share permissions",
Error: errors.New("cannot set the requested share permissions"),
}
}
req := link.CreatePublicShareRequest{
ResourceInfo: statInfo,
Grant: &link.Grant{
Permissions: &link.PublicSharePermissions{
Permissions: permissions,
},
Password: password,
},
}
expireTimeString, ok := r.Form["expireDate"]
if ok {
if expireTimeString[0] != "" {
expireTime, err := conversions.ParseTimestamp(expireTimeString[0])
if err != nil {
return nil, &ocsError{
Code: response.MetaBadRequest.StatusCode,
Message: err.Error(),
Error: err,
}
}
if expireTime != nil {
req.Grant.Expiration = expireTime
}
}
}
// set displayname and password protected as arbitrary metadata
req.ResourceInfo.ArbitraryMetadata = &provider.ArbitraryMetadata{
Metadata: map[string]string{
"name": r.FormValue("name"),
"quicklink": r.FormValue("quicklink"),
},
}
createRes, err := c.CreatePublicShare(ctx, &req)
if err != nil {
log.Debug().Err(err).Str("createShare", "shares").Msgf("error creating a public share to resource id: %v", statInfo.GetId())
return nil, &ocsError{
Code: response.MetaServerError.StatusCode,
Message: "error creating public share",
Error: fmt.Errorf("error creating a public share to resource id: %v", statInfo.GetId()),
}
}
if createRes.Status.Code != rpc.Code_CODE_OK {
log.Debug().Err(errors.New("create public share failed")).Str("shares", "createShare").Msgf("create public share failed with status code: %v", createRes.Status.Code.String())
return nil, &ocsError{
Code: response.MetaServerError.StatusCode,
Message: "grpc create public share request failed",
Error: nil,
}
}
return createRes.Share, nil
}
func (h *Handler) listPublicShares(r *http.Request, filters []*link.ListPublicSharesRequest_Filter) ([]*conversions.ShareData, *rpc.Status, error) {
ctx := r.Context()
log := appctx.GetLogger(ctx)
ocsDataPayload := make([]*conversions.ShareData, 0)
// TODO(refs) why is this guard needed? Are we moving towards a gateway only for service discovery? without a gateway this is dead code.
if h.gatewayAddr != "" {
client, err := pool.GetGatewayServiceClient(h.gatewayAddr)
if err != nil {
return ocsDataPayload, nil, err
}
req := link.ListPublicSharesRequest{
Filters: filters,
}
res, err := client.ListPublicShares(ctx, &req)
if err != nil {
return ocsDataPayload, nil, err
}
if res.Status.Code != rpc.Code_CODE_OK {
return ocsDataPayload, res.Status, nil
}
for _, share := range res.GetShare() {
info, status, err := h.getResourceInfoByID(ctx, client, share.ResourceId)
if err != nil || status.Code != rpc.Code_CODE_OK {
log.Debug().Interface("share", share).Interface("status", status).Err(err).Msg("could not stat share, skipping")
continue
}
sData := conversions.PublicShare2ShareData(share, r, h.publicURL)
sData.Name = share.DisplayName
h.addFileInfo(ctx, sData, info)
h.mapUserIds(ctx, client, sData)
log.Debug().Interface("share", share).Interface("info", info).Interface("shareData", share).Msg("mapped")
ocsDataPayload = append(ocsDataPayload, sData)
}
return ocsDataPayload, nil, nil
}
return ocsDataPayload, nil, errors.New("bad request")
}
func (h *Handler) isPublicShare(r *http.Request, oid string) (*link.PublicShare, bool) {
logger := appctx.GetLogger(r.Context())
client, err := h.getClient()
if err != nil {
logger.Err(err)
return nil, false
}
psRes, err := client.GetPublicShare(r.Context(), &link.GetPublicShareRequest{
Ref: &link.PublicShareReference{
Spec: &link.PublicShareReference_Id{
Id: &link.PublicShareId{
OpaqueId: oid,
},
},
},
})
switch {
case err != nil:
log.Err(err).Send()
case psRes.Status.Code == rpc.Code_CODE_OK:
return psRes.GetShare(), psRes.GetShare() != nil
case psRes.Status.Code == rpc.Code_CODE_INTERNAL:
log.Error().Str("message", psRes.GetStatus().GetMessage()).Str("code", psRes.GetStatus().GetCode().String()).Msg("isPublicShare received internal error")
default:
log.Debug().Str("message", psRes.GetStatus().GetMessage()).Str("code", psRes.GetStatus().GetCode().String()).Msg("isPublicShare received unexpected status")
}
return nil, false
}
func (h *Handler) updatePublicShare(w http.ResponseWriter, r *http.Request, share *link.PublicShare) {
updates := []*link.UpdatePublicShareRequest_Update{}
logger := appctx.GetLogger(r.Context())
gwC, err := h.getClient()
if err != nil {
log.Err(err).Str("shareID", share.GetId().GetOpaqueId()).Msg("updatePublicShare")
response.WriteOCSError(w, r, response.MetaBadRequest.StatusCode, "error getting a connection to the gateway service", nil)
return
}
ctx := r.Context()
user := ctxpkg.ContextMustGetUser(ctx)
permKey, err := permKeyFromRequest(r, h)
if err != nil {
response.WriteOCSError(w, r, response.MetaBadRequest.StatusCode, "invalid permissions", err)
return
}
createdByUser := publicshare.IsCreatedByUser(share, user)
// NOTE: you are allowed to update a link TO a public link without the `PublicLink.Write` permission if you created it yourself
if (permKey != nil && *permKey != 0) || !createdByUser {
ok, err := utils.CheckPermission(ctx, permission.WritePublicLink, gwC)
if err != nil {
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "failed to check user permission", err)
return
}
if !ok {
response.WriteOCSError(w, r, response.MetaForbidden.StatusCode, "user is not allowed to update the public link", nil)
return
}
}
if !createdByUser {
sRes, err := gwC.Stat(r.Context(), &provider.StatRequest{Ref: &provider.Reference{ResourceId: share.ResourceId}})
if err != nil {
log.Err(err).Interface("resource_id", share.ResourceId).Msg("failed to stat shared resource")
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "failed to get public share", nil)
return
}
if sRes.Info == nil || !sRes.Info.GetPermissionSet().UpdateGrant {
response.WriteOCSError(w, r, response.MetaUnauthorized.StatusCode, "missing permissions to update share", err)
return
}
}
err = r.ParseForm()
if err != nil {
response.WriteOCSError(w, r, response.MetaBadRequest.StatusCode, "Could not parse form from request", err)
return
}
// indicates whether values to update were found,
// to check if the request was valid,
// not whether an actual update has been performed
updatesFound := false
newName, ok := r.Form["name"]
if ok {
updatesFound = true
if newName[0] != share.DisplayName {
updates = append(updates, &link.UpdatePublicShareRequest_Update{
Type: link.UpdatePublicShareRequest_Update_TYPE_DISPLAYNAME,
DisplayName: newName[0],
})
}
}
// Permissions
newPermissions, err := ocPublicPermToCs3(permKey)
logger.Debug().Interface("newPermissions", newPermissions).Msg("Parsed permissions")
if err != nil {
response.WriteOCSError(w, r, response.MetaBadRequest.StatusCode, "invalid permissions", err)
return
}
// update permissions if given
if newPermissions != nil {
updatesFound = true
publicSharePermissions := &link.PublicSharePermissions{
Permissions: newPermissions,
}
beforePerm, _ := json.Marshal(share.Permissions)
afterPerm, _ := json.Marshal(publicSharePermissions)
if string(beforePerm) != string(afterPerm) {
logger.Info().Str("shares", "update").Msgf("updating permissions from %v to: %v", string(beforePerm), string(afterPerm))
updates = append(updates, &link.UpdatePublicShareRequest_Update{
Type: link.UpdatePublicShareRequest_Update_TYPE_PERMISSIONS,
Grant: &link.Grant{
Permissions: publicSharePermissions,
Password: r.FormValue("password"),
},
})
}
}
statReq := provider.StatRequest{Ref: &provider.Reference{ResourceId: share.ResourceId}}
statRes, err := gwC.Stat(r.Context(), &statReq)
if err != nil {
log.Debug().Err(err).Str("shares", "update public share").Msg("error during stat")
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "missing resource information", fmt.Errorf("error getting resource information"))
return
}
if statRes.GetStatus().GetCode() != rpc.Code_CODE_OK {
if statRes.GetStatus().GetCode() == rpc.Code_CODE_NOT_FOUND {
response.WriteOCSError(w, r, response.MetaNotFound.StatusCode, "update public share: resource not found", err)
return
}
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "grpc stat request failed for stat when updating a public share", err)
return
}
// empty permissions mean internal link here - NOT denial. Hence we need an extra check
if newPermissions != nil {
if !sufficientPermissions(statRes.GetInfo().GetPermissionSet(), newPermissions, true) {
response.WriteOCSError(w, r, http.StatusForbidden, "no share permission", nil)
return
}
} else {
statRes.GetInfo().GetPermissionSet()
p := decreasePermissionsIfNecessary(int(conversions.RoleFromResourcePermissions(statRes.GetInfo().GetPermissionSet(), false).OCSPermissions()))
permKey = &p
}
// ExpireDate
expireTimeString, ok := r.Form["expireDate"]
// check if value is set and must be updated or cleared
if ok {
updatesFound = true
var newExpiration *types.Timestamp
if expireTimeString[0] != "" {
newExpiration, err = conversions.ParseTimestamp(expireTimeString[0])
if err != nil {
response.WriteOCSError(w, r, response.MetaBadRequest.StatusCode, "invalid datetime format", err)
return
}
}
beforeExpiration, _ := json.Marshal(share.Expiration)
afterExpiration, _ := json.Marshal(newExpiration)
if string(afterExpiration) != string(beforeExpiration) {
logger.Debug().Str("shares", "update").Msgf("updating expire date from %v to: %v", string(beforeExpiration), string(afterExpiration))
updates = append(updates, &link.UpdatePublicShareRequest_Update{
Type: link.UpdatePublicShareRequest_Update_TYPE_EXPIRATION,
Grant: &link.Grant{
Expiration: newExpiration,
},
})
}
}
// Password
newPassword, ok := r.Form["password"]
// enforcePassword
if h.enforcePassword(permKey) {
p, err := conversions.NewPermissions(decreasePermissionsIfNecessary(*permKey))
if err != nil {
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "failed to check permissions from request", err)
return
}
if !ok && !share.PasswordProtected || ok && len(newPassword[0]) == 0 {
if h.checkPasswordEnforcement(ctx, user, p, w, r) != nil {
return
}
}
}
// update or clear password
if ok {
// skip validation if the clear password scenario
if len(newPassword[0]) > 0 {
if err := h.passwordValidator.Validate(newPassword[0]); err != nil {
response.WriteOCSError(w, r, response.MetaBadRequest.StatusCode, xstrings.FirstRuneToUpper(err.Error()), err)
return
}
}
updatesFound = true
logger.Info().Str("shares", "update").Msg("password updated")
updates = append(updates, &link.UpdatePublicShareRequest_Update{
Type: link.UpdatePublicShareRequest_Update_TYPE_PASSWORD,
Grant: &link.Grant{
Password: newPassword[0],
},
})
}
// Updates are atomical. See: https://github.com/cs3org/cs3apis/pull/67#issuecomment-617651428 so in order to get the latest updated version
if len(updates) > 0 {
uRes := &link.UpdatePublicShareResponse{Share: share}
for k := range updates {
uRes, err = gwC.UpdatePublicShare(r.Context(), &link.UpdatePublicShareRequest{
Ref: &link.PublicShareReference{
Spec: &link.PublicShareReference_Id{
Id: &link.PublicShareId{
OpaqueId: share.Id.OpaqueId,
},
},
},
Update: updates[k],
})
if err != nil {
log.Err(err).Str("shareID", share.Id.OpaqueId).Msg("sending update request to public link provider")
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "Error sending update request to public link provider", err)
return
}
if uRes.Status.Code != rpc.Code_CODE_OK {
log.Debug().Str("shareID", share.Id.OpaqueId).Msgf("sending update request to public link provider failed: %s", uRes.Status.Message)
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, fmt.Sprintf("Error sending update request to public link provider: %s", uRes.Status.Message), nil)
return
}
}
share = uRes.Share
} else if !updatesFound {
response.WriteOCSError(w, r, response.MetaBadRequest.StatusCode, "No updates specified in request", nil)
return
}
s := conversions.PublicShare2ShareData(share, r, h.publicURL)
h.addFileInfo(r.Context(), s, statRes.Info)
h.mapUserIds(r.Context(), gwC, s)
response.WriteOCSSuccess(w, r, s)
}
func (h *Handler) removePublicShare(w http.ResponseWriter, r *http.Request, share *link.PublicShare) {
ctx := r.Context()
c, err := h.getClient()
if err != nil {
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error getting grpc gateway client", err)
return
}
u := ctxpkg.ContextMustGetUser(ctx)
if !publicshare.IsCreatedByUser(share, u) {
sRes, err := c.Stat(r.Context(), &provider.StatRequest{Ref: &provider.Reference{ResourceId: share.ResourceId}})
if err != nil {
log.Err(err).Interface("resource_id", share.ResourceId).Msg("failed to stat shared resource")
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "failed to get public share", nil)
return
}
if !sRes.Info.PermissionSet.RemoveGrant {
response.WriteOCSError(w, r, response.MetaUnauthorized.StatusCode, "missing permissions to remove share", err)
return
}
}
req := &link.RemovePublicShareRequest{
Ref: &link.PublicShareReference{
Spec: &link.PublicShareReference_Id{
Id: &link.PublicShareId{
OpaqueId: share.GetId().GetOpaqueId(),
},
},
},
}
res, err := c.RemovePublicShare(ctx, req)
if err != nil {
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error sending a grpc delete share request", err)
return
}
if res.Status.Code != rpc.Code_CODE_OK {
if res.Status.Code == rpc.Code_CODE_NOT_FOUND {
response.WriteOCSError(w, r, response.MetaNotFound.StatusCode, "not found", nil)
return
}
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "grpc delete share request failed", err)
return
}
response.WriteOCSSuccess(w, r, nil)
}
// enforcePassword validate Password enforce based on configuration
// read_only: 1
// read_write: 3 or 5
// read_write_delete: 15
// upload_only: 4
func (h *Handler) enforcePassword(pk *int) bool {
if pk == nil {
return false
}
p, err := conversions.NewPermissions(decreasePermissionsIfNecessary(*pk))
if err != nil {
return false
}
if h.publicPasswordEnforced.EnforcedForReadOnly &&
p == conversions.PermissionRead {
return true
}
if h.publicPasswordEnforced.EnforcedForReadWrite &&
(p == conversions.PermissionRead|conversions.PermissionWrite ||
p == conversions.PermissionRead|conversions.PermissionCreate) {
return true
}
if h.publicPasswordEnforced.EnforcedForReadWriteDelete &&
p == conversions.PermissionRead|conversions.PermissionWrite|conversions.PermissionCreate|conversions.PermissionDelete {
return true
}
if h.publicPasswordEnforced.EnforcedForUploadOnly &&
p == conversions.PermissionCreate {
return true
}
return false
}
// for public links oc10 api decreases all permissions to read: stay compatible!
func decreasePermissionsIfNecessary(perm int) int {
if perm == int(conversions.PermissionAll) {
perm = int(conversions.PermissionRead)
}
return perm
}
func ocPublicPermToCs3(pk *int) (*provider.ResourcePermissions, error) {
if pk == nil {
return nil, nil
}
permKey := decreasePermissionsIfNecessary(*pk)
// TODO refactor this ocPublicPermToRole[permKey] check into a conversions.NewPublicSharePermissions?
// not all permissions are possible for public shares
_, ok := ocPublicPermToRole[permKey]
if !ok {
log.Error().Str("ocPublicPermToCs3", "shares").Int("perm", permKey).Msg("invalid public share permission")
return nil, fmt.Errorf("invalid public share permission: %d", permKey)
}
perm, err := conversions.NewPermissions(permKey)
if err != nil && err != conversions.ErrZeroPermission { // we allow empty permissions for public links
return nil, err
}
return conversions.RoleFromOCSPermissions(perm, nil).CS3ResourcePermissions(), nil
}
// pointer will be nil if no permission is set
func permKeyFromRequest(r *http.Request, h *Handler) (*int, error) {
var err error
// phoenix sends: {"permissions": 15}. See ocPublicPermToRole struct for mapping
permKey := 1
// note: "permissions" value has higher priority than "publicUpload"
// handle legacy "publicUpload" arg that overrides permissions differently depending on the scenario
// https://github.com/owncloud/core/blob/v10.4.0/apps/files_sharing/lib/Controller/Share20OcsController.php#L447
publicUploadString := r.FormValue("publicUpload")
if publicUploadString != "" {
publicUploadFlag, err := strconv.ParseBool(publicUploadString)
if err != nil {
log.Error().Err(err).Str("publicUpload", publicUploadString).Msg("could not parse publicUpload argument")
return nil, err
}
if publicUploadFlag {
// all perms except reshare
permKey = 15
}
} else {
permissionsString := r.FormValue("permissions")
if permissionsString == "" {
// no permission values given
return nil, nil
}
permKey, err = strconv.Atoi(permissionsString)
if err != nil {
log.Error().Str("permissionFromRequest", "shares").Msgf("invalid type: %T", permKey)
return nil, fmt.Errorf("invalid type: %T", permKey)
}
}
return &permKey, nil
}
// checkPasswordEnforcement checks if the password needs to be set for a link
// some users can opt out of the enforcement based on a user permission
func (h *Handler) checkPasswordEnforcement(ctx context.Context, user *userv1beta1.User, perm conversions.Permissions, w http.ResponseWriter, r *http.Request) error {
// Non-read-only links
if perm != conversions.PermissionRead {
response.WriteOCSError(w, r, response.MetaBadRequest.StatusCode, "missing required password", nil)
return errors.New("missing required password")
}
// Check if the user is allowed to opt out of the password enforcement
// for read-only links
gwC, err := h.getClient()
if err != nil {
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "could not check permission", err)
return errors.New("could not check permission")
}
ok, err := utils.CheckPermission(ctx, permission.DeleteReadOnlyPassword, gwC)
if err != nil {
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "failed to check user permission", err)
return errors.New("failed to check user permission")
}
if !ok {
response.WriteOCSError(w, r, response.MetaForbidden.StatusCode, "user is not allowed to delete the password from the public link", nil)
return errors.New("user is not allowed to delete the password from the public link")
}
return nil
}
// TODO: add mapping for user share permissions to role
// Maps oc10 public link permissions to roles
var ocPublicPermToRole = map[int]string{
// Recipients can do nothing
0: "none",
// Recipients can view and download contents.
1: "viewer",
// Recipients can view, download and edit single files.
3: "file-editor",
// Recipients can view, download, edit, delete and upload contents
15: "editor",
// Recipients can upload but existing contents are not revealed
4: "uploader",
// Recipients can view, download and upload contents
5: "contributor",
}
@@ -0,0 +1,300 @@
// Copyright 2018-2023 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 shares
import (
"context"
"net/http"
"path/filepath"
"strings"
providerv1beta1 "github.com/cs3org/go-cs3apis/cs3/app/provider/v1beta1"
gatewayv1beta1 "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
invitepb "github.com/cs3org/go-cs3apis/cs3/ocm/invite/v1beta1"
providerpb "github.com/cs3org/go-cs3apis/cs3/ocm/provider/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
ocm "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/go-chi/chi/v5"
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocs/response"
"github.com/opencloud-eu/reva/v2/pkg/conversions"
"github.com/opencloud-eu/reva/v2/pkg/ocm/share"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/pkg/errors"
)
func (h *Handler) createFederatedCloudShare(w http.ResponseWriter, r *http.Request, resource *provider.ResourceInfo, role *conversions.Role, roleVal []byte) {
ctx := r.Context()
c, err := pool.GetGatewayServiceClient(h.gatewayAddr)
if err != nil {
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error getting grpc gateway client", err)
return
}
shareWithUser, shareWithProvider := r.FormValue("shareWith"), r.FormValue("shareWithProvider")
if shareWithUser == "" || shareWithProvider == "" {
response.WriteOCSError(w, r, response.MetaBadRequest.StatusCode, "missing shareWith parameters", nil)
return
}
providerInfoResp, err := c.GetInfoByDomain(ctx, &providerpb.GetInfoByDomainRequest{
Domain: shareWithProvider,
})
if err != nil {
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error sending a grpc get invite by domain info request", err)
return
}
if providerInfoResp.Status.Code != rpc.Code_CODE_OK {
// return proper error
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error from provider info response", errors.New(providerInfoResp.Status.Message))
return
}
remoteUserRes, err := c.GetAcceptedUser(ctx, &invitepb.GetAcceptedUserRequest{
RemoteUserId: &userpb.UserId{OpaqueId: shareWithUser, Idp: shareWithProvider, Type: userpb.UserType_USER_TYPE_FEDERATED},
})
if err != nil {
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error searching recipient", err)
return
}
if remoteUserRes.Status.Code != rpc.Code_CODE_OK {
response.WriteOCSError(w, r, response.MetaNotFound.StatusCode, "user not found", err)
return
}
createShareResponse, err := c.CreateOCMShare(ctx, &ocm.CreateOCMShareRequest{
ResourceId: resource.Id,
Grantee: &provider.Grantee{
Type: provider.GranteeType_GRANTEE_TYPE_USER,
Id: &provider.Grantee_UserId{
UserId: remoteUserRes.RemoteUser.Id,
},
},
RecipientMeshProvider: providerInfoResp.ProviderInfo,
AccessMethods: []*ocm.AccessMethod{
share.NewWebDavAccessMethod(role.CS3ResourcePermissions()),
share.NewWebappAccessMethod(getViewModeFromRole(role)),
},
})
if err != nil {
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error sending a grpc create ocm share request", err)
return
}
if createShareResponse.Status.Code != rpc.Code_CODE_OK {
if createShareResponse.Status.Code == rpc.Code_CODE_NOT_FOUND {
response.WriteOCSError(w, r, response.MetaNotFound.StatusCode, "not found", nil)
return
}
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "grpc create ocm share request failed", err)
return
}
s := createShareResponse.Share
data, err := conversions.OCMShare2ShareData(s)
if err != nil {
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error converting share", err)
return
}
h.mapUserIdsFederatedShare(ctx, c, data)
info, status, err := h.getResourceInfoByID(ctx, c, s.ResourceId)
if err != nil {
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error statting resource id", err)
return
}
if status.Code != rpc.Code_CODE_OK {
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error statting resource id", errors.New(status.Message))
return
}
h.addFileInfo(ctx, data, info)
response.WriteOCSSuccess(w, r, data)
}
func getViewModeFromRole(role *conversions.Role) providerv1beta1.ViewMode {
switch role.Name {
case conversions.RoleViewer:
return providerv1beta1.ViewMode_VIEW_MODE_READ_ONLY
case conversions.RoleEditor:
return providerv1beta1.ViewMode_VIEW_MODE_READ_WRITE
}
return providerv1beta1.ViewMode_VIEW_MODE_INVALID
}
// GetFederatedShare handles GET requests on /apps/files_sharing/api/v1/shares/remote_shares/{shareid}.
func (h *Handler) GetFederatedShare(w http.ResponseWriter, r *http.Request) {
// TODO: Implement response with HAL schemating
ctx := r.Context()
shareID := chi.URLParam(r, "shareid")
gatewayClient, err := pool.GetGatewayServiceClient(h.gatewayAddr)
if err != nil {
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error getting grpc gateway client", err)
return
}
listOCMSharesRequest := &ocm.GetOCMShareRequest{
Ref: &ocm.ShareReference{
Spec: &ocm.ShareReference_Id{
Id: &ocm.ShareId{
OpaqueId: shareID,
},
},
},
}
ocmShareResponse, err := gatewayClient.GetOCMShare(ctx, listOCMSharesRequest)
if err != nil {
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error sending a grpc get ocm share request", err)
return
}
share := ocmShareResponse.GetShare()
if share == nil {
response.WriteOCSError(w, r, response.MetaNotFound.StatusCode, "share not found", err)
return
}
response.WriteOCSSuccess(w, r, share)
}
// ListFederatedShares handles GET requests on /apps/files_sharing/api/v1/shares/remote_shares.
func (h *Handler) ListFederatedShares(w http.ResponseWriter, r *http.Request) {
// TODO Implement pagination.
// TODO Implement response with HAL schemating
}
func (h *Handler) listReceivedFederatedShares(ctx context.Context, gw gatewayv1beta1.GatewayAPIClient, state ocm.ShareState) ([]*conversions.ShareData, error) {
listRes, err := gw.ListReceivedOCMShares(ctx, &ocm.ListReceivedOCMSharesRequest{})
if err != nil {
return nil, err
}
shares := []*conversions.ShareData{}
for _, s := range listRes.Shares {
if state != ocsStateUnknown && s.State != state {
continue
}
sd, err := conversions.ReceivedOCMShare2ShareData(s, h.ocmLocalMount(s))
if err != nil {
continue
}
h.mapUserIdsReceivedFederatedShare(ctx, gw, sd)
sd.State = mapOCMState(s.State)
shares = append(shares, sd)
}
return shares, nil
}
func (h *Handler) ocmLocalMount(share *ocm.ReceivedShare) string {
return filepath.Join("/", h.ocmMountPoint, share.Id.OpaqueId)
}
func (h *Handler) mapUserIdsReceivedFederatedShare(ctx context.Context, gw gatewayv1beta1.GatewayAPIClient, sd *conversions.ShareData) {
if sd.ShareWith != "" {
user := h.mustGetIdentifiers(ctx, gw, sd.ShareWith, false)
sd.ShareWith = user.Username
sd.ShareWithDisplayname = user.DisplayName
}
if sd.UIDOwner != "" {
user := h.mustGetRemoteUser(ctx, gw, sd.UIDOwner)
sd.DisplaynameOwner = user.DisplayName
}
if sd.UIDFileOwner != "" {
user := h.mustGetRemoteUser(ctx, gw, sd.UIDFileOwner)
sd.DisplaynameFileOwner = user.DisplayName
}
}
func (h *Handler) mapUserIdsFederatedShare(ctx context.Context, gw gatewayv1beta1.GatewayAPIClient, sd *conversions.ShareData) {
if sd.ShareWith != "" {
user := h.mustGetRemoteUser(ctx, gw, sd.ShareWith)
sd.ShareWith = user.Username
sd.ShareWithDisplayname = user.DisplayName
}
if sd.UIDOwner != "" {
user := h.mustGetIdentifiers(ctx, gw, sd.UIDOwner, false)
sd.DisplaynameOwner = user.DisplayName
}
if sd.UIDFileOwner != "" {
user := h.mustGetIdentifiers(ctx, gw, sd.UIDFileOwner, false)
sd.DisplaynameFileOwner = user.DisplayName
}
}
func (h *Handler) mustGetRemoteUser(ctx context.Context, gw gatewayv1beta1.GatewayAPIClient, id string) *userIdentifiers {
s := strings.SplitN(id, "@", 2)
opaqueID, idp := s[0], s[1]
userRes, err := gw.GetAcceptedUser(ctx, &invitepb.GetAcceptedUserRequest{
RemoteUserId: &userpb.UserId{
Idp: idp,
OpaqueId: opaqueID,
},
})
if err != nil {
return &userIdentifiers{}
}
if userRes.Status.Code != rpc.Code_CODE_OK {
return &userIdentifiers{}
}
user := userRes.RemoteUser
return &userIdentifiers{
DisplayName: user.DisplayName,
Username: user.Username,
Mail: user.Mail,
}
}
func (h *Handler) listOutcomingFederatedShares(ctx context.Context, gw gatewayv1beta1.GatewayAPIClient, filters []*ocm.ListOCMSharesRequest_Filter) ([]*conversions.ShareData, error) {
listRes, err := gw.ListOCMShares(ctx, &ocm.ListOCMSharesRequest{
Filters: filters,
})
if err != nil {
return nil, err
}
shares := []*conversions.ShareData{}
for _, s := range listRes.Shares {
sd, err := conversions.OCMShare2ShareData(s)
if err != nil {
continue
}
h.mapUserIdsFederatedShare(ctx, gw, sd)
info, status, err := h.getResourceInfoByID(ctx, gw, s.ResourceId)
if err != nil {
return nil, err
}
if status.Code != rpc.Code_CODE_OK {
return nil, err
}
h.addFileInfo(ctx, sd, info)
shares = append(shares, sd)
}
return shares, nil
}
@@ -0,0 +1,391 @@
// Copyright 2018-2021 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 shares
import (
"context"
"fmt"
"net/http"
"time"
groupv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/group/v1beta1"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
collaborationv1beta1 "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
registry "github.com/cs3org/go-cs3apis/cs3/storage/registry/v1beta1"
types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocs/response"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
"github.com/opencloud-eu/reva/v2/pkg/conversions"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
sdk "github.com/opencloud-eu/reva/v2/pkg/sdk/common"
"github.com/opencloud-eu/reva/v2/pkg/storagespace"
"github.com/opencloud-eu/reva/v2/pkg/utils"
"github.com/pkg/errors"
"google.golang.org/protobuf/types/known/fieldmaskpb"
)
func (h *Handler) getGrantee(ctx context.Context, name string) (provider.Grantee, error) {
log := appctx.GetLogger(ctx)
client, err := h.getClient()
if err != nil {
return provider.Grantee{}, err
}
userRes, err := client.GetUserByClaim(ctx, &userpb.GetUserByClaimRequest{
Claim: "username",
Value: name,
})
if err == nil && userRes.Status.Code == rpc.Code_CODE_OK {
return provider.Grantee{
Type: provider.GranteeType_GRANTEE_TYPE_USER,
Id: &provider.Grantee_UserId{UserId: userRes.User.Id},
}, nil
}
log.Debug().Str("name", name).Msg("no user found")
groupRes, err := client.GetGroupByClaim(ctx, &groupv1beta1.GetGroupByClaimRequest{
Claim: "group_name",
Value: name,
SkipFetchingMembers: true,
})
if err == nil && groupRes.Status.Code == rpc.Code_CODE_OK {
return provider.Grantee{
Type: provider.GranteeType_GRANTEE_TYPE_GROUP,
Id: &provider.Grantee_GroupId{GroupId: groupRes.Group.Id},
}, nil
}
log.Debug().Str("name", name).Msg("no group found")
return provider.Grantee{}, fmt.Errorf("no grantee found with name %s", name)
}
func (h *Handler) addSpaceMember(w http.ResponseWriter, r *http.Request, info *provider.ResourceInfo, role *conversions.Role, roleVal []byte) {
ctx := r.Context()
if info.Space.SpaceType == "personal" {
response.WriteOCSError(w, r, response.MetaBadRequest.StatusCode, "can not add members to personal spaces", nil)
return
}
shareWith := r.FormValue("shareWith")
if shareWith == "" {
response.WriteOCSError(w, r, response.MetaBadRequest.StatusCode, "missing shareWith", nil)
return
}
grantee, err := h.getGrantee(ctx, shareWith)
if err != nil {
response.WriteOCSError(w, r, response.MetaNotFound.StatusCode, "error getting grantee", err)
return
}
client, err := h.getClient()
if err != nil {
response.WriteOCSError(w, r, response.MetaNotFound.StatusCode, "error getting gateway client", err)
return
}
permissions := role.CS3ResourcePermissions()
// All members of a space should be able to list shares inside that space.
// The viewer role doesn't have the ListGrants permission so we set it here.
permissions.ListGrants = true
fieldmask := []string{}
expireDate := r.PostFormValue("expireDate")
var expirationTs *types.Timestamp
fieldmask = append(fieldmask, "expiration")
if expireDate != "" {
expiration, err := time.Parse(_iso8601, expireDate)
if err != nil {
// Web sends different formats when adding and when editing a space membership...
// We need to fix this in a separate PR.
expiration, err = time.Parse(time.RFC3339, expireDate)
if err != nil {
response.WriteOCSError(w, r, response.MetaBadRequest.StatusCode, "could not parse expireDate", err)
return
}
}
expirationTs = &types.Timestamp{
Seconds: uint64(expiration.UnixNano() / int64(time.Second)),
Nanos: uint32(expiration.UnixNano() % int64(time.Second)),
}
fieldmask = append(fieldmask, "expiration")
}
ref := provider.Reference{ResourceId: info.GetId()}
p, err := h.findProvider(ctx, &ref)
if err != nil {
response.WriteOCSError(w, r, response.MetaNotFound.StatusCode, "error getting storage provider", err)
return
}
providerClient, err := h.getStorageProviderClient(p)
if err != nil {
response.WriteOCSError(w, r, response.MetaNotFound.StatusCode, "error getting storage provider client", err)
return
}
lgRes, err := providerClient.ListGrants(ctx, &provider.ListGrantsRequest{Ref: &ref})
if err != nil || lgRes.Status.Code != rpc.Code_CODE_OK {
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error listing space grants", err)
return
}
if !isSpaceManagerRemaining(lgRes.Grants, &grantee) {
response.WriteOCSError(w, r, http.StatusForbidden, "the space must have at least one manager", nil)
return
}
// we have to send the update request to the gateway to give it a chance to invalidate its cache
// TODO the gateway no longer should cache stuff because invalidation is to expensive. The decomposedfs already has a better cache.
if granteeExists(lgRes.Grants, &grantee) {
if permissions != nil {
fieldmask = append(fieldmask, "permissions")
}
updateShareReq := &collaborationv1beta1.UpdateShareRequest{
// TODO: change CS3 APIs
Opaque: &types.Opaque{
Map: map[string]*types.OpaqueEntry{
"spacegrant": {},
},
},
Share: &collaborationv1beta1.Share{
ResourceId: ref.GetResourceId(),
Permissions: &collaborationv1beta1.SharePermissions{
Permissions: permissions,
},
Grantee: &grantee,
Expiration: expirationTs,
},
UpdateMask: &fieldmaskpb.FieldMask{
Paths: fieldmask,
},
}
updateShareReq.Opaque = utils.AppendPlainToOpaque(updateShareReq.Opaque, "spacetype", info.GetSpace().GetSpaceType())
updateShareRes, err := client.UpdateShare(ctx, updateShareReq)
if err != nil || updateShareRes.Status.Code != rpc.Code_CODE_OK {
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "could not update space member grant", err)
return
}
} else {
createShareRes, err := client.CreateShare(ctx, &collaborationv1beta1.CreateShareRequest{
ResourceInfo: info,
Grant: &collaborationv1beta1.ShareGrant{
Permissions: &collaborationv1beta1.SharePermissions{
Permissions: permissions,
},
Grantee: &grantee,
Expiration: expirationTs,
},
})
if err != nil || createShareRes.Status.Code != rpc.Code_CODE_OK {
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "could not add space member grant", err)
return
}
}
response.WriteOCSSuccess(w, r, nil)
}
func (h *Handler) isSpaceShare(r *http.Request, spaceID string) (*registry.ProviderInfo, bool) {
ref, err := storagespace.ParseReference(spaceID)
if err != nil {
return nil, false
}
if ref.ResourceId.OpaqueId == "" {
ref.ResourceId.OpaqueId = ref.ResourceId.SpaceId
}
p, err := h.findProvider(r.Context(), &ref)
return p, err == nil
}
func (h *Handler) removeSpaceMember(w http.ResponseWriter, r *http.Request, spaceID string, prov *registry.ProviderInfo) {
ctx := r.Context()
shareWith := r.URL.Query().Get("shareWith")
if shareWith == "" {
response.WriteOCSError(w, r, response.MetaBadRequest.StatusCode, "missing shareWith", nil)
return
}
grantee, err := h.getGrantee(ctx, shareWith)
if err != nil {
response.WriteOCSError(w, r, response.MetaNotFound.StatusCode, "error getting grantee", err)
return
}
ref, err := storagespace.ParseReference(spaceID)
if err != nil {
response.WriteOCSError(w, r, response.MetaBadRequest.StatusCode, "could not parse space id", err)
return
}
if ref.ResourceId.OpaqueId == "" {
ref.ResourceId.OpaqueId = ref.ResourceId.SpaceId
}
providerClient, err := h.getStorageProviderClient(prov)
if err != nil {
response.WriteOCSError(w, r, response.MetaNotFound.StatusCode, "error getting storage provider client", err)
return
}
lgRes, err := providerClient.ListGrants(ctx, &provider.ListGrantsRequest{Ref: &ref})
if err != nil || lgRes.Status.Code != rpc.Code_CODE_OK {
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error listing space grants", err)
return
}
if len(lgRes.Grants) == 1 || !isSpaceManagerRemaining(lgRes.Grants, &grantee) {
response.WriteOCSError(w, r, http.StatusForbidden, "can't remove the last manager", nil)
return
}
gatewayClient, err := h.getClient()
if err != nil {
response.WriteOCSError(w, r, response.MetaNotFound.StatusCode, "error getting gateway client", err)
return
}
removeShareRes, err := gatewayClient.RemoveShare(ctx, &collaborationv1beta1.RemoveShareRequest{
Ref: &collaborationv1beta1.ShareReference{
Spec: &collaborationv1beta1.ShareReference_Key{
Key: &collaborationv1beta1.ShareKey{
ResourceId: ref.ResourceId,
Grantee: &grantee,
},
},
},
})
if err != nil {
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error removing grant", err)
return
}
if removeShareRes.Status.Code != rpc.Code_CODE_OK {
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error removing grant", err)
return
}
response.WriteOCSSuccess(w, r, nil)
}
func (h *Handler) getStorageProviderClient(p *registry.ProviderInfo) (provider.ProviderAPIClient, error) {
c, err := pool.GetStorageProviderServiceClient(p.Address)
if err != nil {
err = errors.Wrap(err, "shares spaces: error getting a storage provider client")
return nil, err
}
return c, nil
}
func (h *Handler) findProvider(ctx context.Context, ref *provider.Reference) (*registry.ProviderInfo, error) {
c, err := pool.GetStorageRegistryClient(h.storageRegistryAddr)
if err != nil {
return nil, errors.Wrap(err, "shares spaces: error getting storage registry client")
}
filters := map[string]string{}
if ref.Path != "" {
filters["path"] = ref.Path
}
if ref.ResourceId != nil {
filters["storage_id"] = ref.ResourceId.StorageId
filters["space_id"] = ref.ResourceId.SpaceId
filters["opaque_id"] = ref.ResourceId.OpaqueId
}
listReq := &registry.ListStorageProvidersRequest{
Opaque: &types.Opaque{},
}
sdk.EncodeOpaqueMap(listReq.Opaque, filters)
res, err := c.ListStorageProviders(ctx, listReq)
if err != nil {
return nil, errors.Wrap(err, "shares spaces: error calling ListStorageProviders")
}
if res.Status.Code != rpc.Code_CODE_OK {
switch res.Status.Code {
case rpc.Code_CODE_NOT_FOUND:
return nil, errtypes.NotFound("shares spaces: storage provider not found for reference:" + ref.String())
case rpc.Code_CODE_PERMISSION_DENIED:
return nil, errtypes.PermissionDenied("shares spaces: " + res.Status.Message + " for " + ref.String() + " with code " + res.Status.Code.String())
case rpc.Code_CODE_INVALID_ARGUMENT, rpc.Code_CODE_FAILED_PRECONDITION, rpc.Code_CODE_OUT_OF_RANGE:
return nil, errtypes.BadRequest("shares spaces: " + res.Status.Message + " for " + ref.String() + " with code " + res.Status.Code.String())
case rpc.Code_CODE_UNIMPLEMENTED:
return nil, errtypes.NotSupported("shares spaces: " + res.Status.Message + " for " + ref.String() + " with code " + res.Status.Code.String())
default:
return nil, status.NewErrorFromCode(res.Status.Code, "shares spaces")
}
}
if len(res.Providers) < 1 {
return nil, errtypes.NotFound("shares spaces: no provider found")
}
return res.Providers[0], nil
}
func isSpaceManagerRemaining(grants []*provider.Grant, grantee *provider.Grantee) bool {
for _, g := range grants {
// RemoveGrant is currently the way to check for the manager role
// If it is not set than the current grant is not for a manager and
// we can just continue with the next one.
if g.Permissions.RemoveGrant && !isEqualGrantee(g.Grantee, grantee) {
return true
}
}
return false
}
func granteeExists(grants []*provider.Grant, grantee *provider.Grantee) bool {
for _, g := range grants {
if isEqualGrantee(g.Grantee, grantee) {
return true
}
}
return false
}
func isEqualGrantee(a, b *provider.Grantee) bool {
// Ideally we would want to use utils.GranteeEqual()
// but the grants stored in the decomposedfs aren't complete (missing usertype and idp)
// because of that the check would fail so we can only check the ... for now.
if a.Type != b.Type {
return false
}
var aID, bID string
switch a.Type {
case provider.GranteeType_GRANTEE_TYPE_GROUP:
aID = a.GetGroupId().OpaqueId
bID = b.GetGroupId().OpaqueId
case provider.GranteeType_GRANTEE_TYPE_USER:
aID = a.GetUserId().OpaqueId
bID = b.GetUserId().OpaqueId
}
return aID == bID
}
@@ -0,0 +1,421 @@
// Copyright 2018-2021 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 shares
import (
"net/http"
"time"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1"
ocmpb "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocs/response"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
"github.com/opencloud-eu/reva/v2/pkg/conversions"
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
"github.com/opencloud-eu/reva/v2/pkg/permission"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/opencloud-eu/reva/v2/pkg/utils"
)
const (
_iso8601 = "2006-01-02T15:04:05Z0700"
)
func (h *Handler) createUserShare(w http.ResponseWriter, r *http.Request, statInfo *provider.ResourceInfo, role *conversions.Role, roleVal []byte) (*collaboration.Share, *ocsError) {
ctx := r.Context()
c, err := h.getClient()
if err != nil {
return nil, &ocsError{
Code: response.MetaServerError.StatusCode,
Message: "error getting grpc gateway client",
Error: err,
}
}
shareWith := r.FormValue("shareWith")
if shareWith == "" {
return nil, &ocsError{
Code: response.MetaBadRequest.StatusCode,
Message: "missing shareWith",
Error: err,
}
}
userRes, err := c.GetUserByClaim(ctx, &userpb.GetUserByClaimRequest{
Claim: "username",
Value: shareWith,
SkipFetchingUserGroups: true,
})
if err != nil {
return nil, &ocsError{
Code: response.MetaServerError.StatusCode,
Message: "error searching recipient",
Error: err,
}
}
if userRes.Status.Code != rpc.Code_CODE_OK {
return nil, &ocsError{
Code: response.MetaNotFound.StatusCode,
Message: "user not found",
Error: err,
}
}
expireDate := r.PostFormValue("expireDate")
var expirationTs *types.Timestamp
if expireDate != "" {
// FIXME: the web ui sends the RFC3339 format when updating a share but
// initially on creating a share the format ISO 8601 is used.
// OC10 uses RFC3339 in both cases so we should fix the web ui and change it here.
expiration, err := time.Parse(_iso8601, expireDate)
if err != nil {
return nil, &ocsError{
Code: response.MetaBadRequest.StatusCode,
Message: "could not parse expireDate",
Error: err,
}
}
expirationTs = &types.Timestamp{
Seconds: uint64(expiration.UnixNano() / int64(time.Second)),
Nanos: uint32(expiration.UnixNano() % int64(time.Second)),
}
}
createShareReq := &collaboration.CreateShareRequest{
Opaque: &types.Opaque{
Map: map[string]*types.OpaqueEntry{
"role": {
Decoder: "json",
Value: roleVal,
},
},
},
ResourceInfo: statInfo,
Grant: &collaboration.ShareGrant{
Grantee: &provider.Grantee{
Type: provider.GranteeType_GRANTEE_TYPE_USER,
Id: &provider.Grantee_UserId{UserId: userRes.User.GetId()},
},
Permissions: &collaboration.SharePermissions{
Permissions: role.CS3ResourcePermissions(),
},
Expiration: expirationTs,
},
}
share, ocsErr := h.createCs3Share(ctx, w, r, c, createShareReq)
if ocsErr != nil {
return nil, ocsErr
}
return share, nil
}
func (h *Handler) isUserShare(r *http.Request, oid string) (*collaboration.Share, bool) {
log := appctx.GetLogger(r.Context())
client, err := pool.GetGatewayServiceClient(h.gatewayAddr)
if err != nil {
log.Err(err).Send()
}
getShareRes, err := client.GetShare(r.Context(), &collaboration.GetShareRequest{
Ref: &collaboration.ShareReference{
Spec: &collaboration.ShareReference_Id{
Id: &collaboration.ShareId{
OpaqueId: oid,
},
},
},
})
switch {
case err != nil:
log.Err(err).Send()
case getShareRes.Status.Code == rpc.Code_CODE_OK:
return getShareRes.GetShare(), true
case getShareRes.Status.Code == rpc.Code_CODE_INTERNAL:
log.Error().Str("message", getShareRes.GetStatus().GetMessage()).Str("code", getShareRes.GetStatus().GetCode().String()).Msg("isUserShare received internal error")
default:
log.Debug().Str("message", getShareRes.GetStatus().GetMessage()).Str("code", getShareRes.GetStatus().GetCode().String()).Msg("isUserShare received unexpected status")
}
return nil, false
}
func (h *Handler) isFederatedShare(r *http.Request, shareID string) bool {
log := appctx.GetLogger(r.Context())
client, err := pool.GetGatewayServiceClient(h.gatewayAddr)
if err != nil {
log.Err(err).Send()
return false
}
getShareRes, err := client.GetOCMShare(r.Context(), &ocmpb.GetOCMShareRequest{
Ref: &ocmpb.ShareReference{
Spec: &ocmpb.ShareReference_Id{
Id: &ocmpb.ShareId{
OpaqueId: shareID,
},
},
},
})
switch {
case err != nil:
log.Err(err).Send()
case getShareRes.Status.Code == rpc.Code_CODE_OK:
return true
case getShareRes.Status.Code == rpc.Code_CODE_INTERNAL:
log.Error().Str("message", getShareRes.GetStatus().GetMessage()).Str("code", getShareRes.GetStatus().GetCode().String()).Msg("isFederatedShare received internal error")
default:
log.Debug().Str("message", getShareRes.GetStatus().GetMessage()).Str("code", getShareRes.GetStatus().GetCode().String()).Msg("isFederatedShare received unexpected status")
}
return false
}
func (h *Handler) removeFederatedShare(w http.ResponseWriter, r *http.Request, shareID string) {
ctx := r.Context()
client, err := pool.GetGatewayServiceClient(h.gatewayAddr)
if err != nil {
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error getting grpc gateway client", err)
return
}
shareRef := &ocmpb.ShareReference_Id{Id: &ocmpb.ShareId{OpaqueId: shareID}}
// Get the share, so that we can include it in the response.
getShareResp, err := client.GetOCMShare(ctx, &ocmpb.GetOCMShareRequest{Ref: &ocmpb.ShareReference{Spec: shareRef}})
if err != nil {
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error sending a grpc delete share request", err)
return
}
if getShareResp.Status.Code != rpc.Code_CODE_OK {
if getShareResp.Status.Code == rpc.Code_CODE_NOT_FOUND {
response.WriteOCSError(w, r, response.MetaNotFound.StatusCode, "not found", nil)
return
}
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "deleting share failed", err)
return
}
data, err := conversions.OCMShare2ShareData(getShareResp.Share)
if err != nil {
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "deleting share failed", err)
return
}
// A deleted share should not have an ID.
data.ID = ""
uRes, err := client.RemoveOCMShare(ctx, &ocmpb.RemoveOCMShareRequest{Ref: &ocmpb.ShareReference{Spec: shareRef}})
if err != nil {
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error sending a grpc delete share request", err)
return
}
if uRes.Status.Code != rpc.Code_CODE_OK {
if uRes.Status.Code == rpc.Code_CODE_NOT_FOUND {
response.WriteOCSError(w, r, response.MetaNotFound.StatusCode, "not found", nil)
return
}
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "grpc delete share request failed", err)
return
}
response.WriteOCSSuccess(w, r, data)
}
func (h *Handler) isFederatedReceivedShare(r *http.Request, shareID string) bool {
log := appctx.GetLogger(r.Context())
client, err := pool.GetGatewayServiceClient(h.gatewayAddr)
if err != nil {
log.Err(err).Send()
return false
}
getShareRes, err := client.GetReceivedOCMShare(r.Context(), &ocmpb.GetReceivedOCMShareRequest{
Ref: &ocmpb.ShareReference{
Spec: &ocmpb.ShareReference_Id{
Id: &ocmpb.ShareId{
OpaqueId: shareID,
},
},
},
})
switch {
case err != nil:
log.Err(err).Send()
case getShareRes.Status.Code == rpc.Code_CODE_OK:
return true
case getShareRes.Status.Code == rpc.Code_CODE_INTERNAL:
log.Error().Str("message", getShareRes.GetStatus().GetMessage()).Str("code", getShareRes.GetStatus().GetCode().String()).Msg("isFederatedReceivedShare received internal error")
default:
log.Debug().Str("message", getShareRes.GetStatus().GetMessage()).Str("code", getShareRes.GetStatus().GetCode().String()).Msg("isFederatedReceivedShare received unexpected status")
}
return false
}
func (h *Handler) removeUserShare(w http.ResponseWriter, r *http.Request, share *collaboration.Share) {
ctx := r.Context()
uClient, err := h.getClient()
if err != nil {
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error getting grpc gateway client", err)
return
}
// TODO: should we use Share.Delete here?
ok, err := utils.CheckPermission(ctx, permission.WriteShare, uClient)
if err != nil {
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error checking user permissions", err)
return
}
if !ok {
response.WriteOCSError(w, r, response.MetaForbidden.StatusCode, "permission denied", nil)
return
}
shareRef := &collaboration.ShareReference{
Spec: &collaboration.ShareReference_Id{
Id: share.Id,
},
}
data := conversions.CS3Share2ShareData(ctx, share)
// A deleted share should not have an ID.
data.ID = ""
uReq := &collaboration.RemoveShareRequest{Ref: shareRef}
uRes, err := uClient.RemoveShare(ctx, uReq)
if err != nil {
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error sending a grpc delete share request", err)
return
}
if uRes.Status.Code != rpc.Code_CODE_OK {
switch uRes.Status.Code {
case rpc.Code_CODE_NOT_FOUND:
response.WriteOCSError(w, r, response.MetaNotFound.StatusCode, "not found", nil)
return
case rpc.Code_CODE_LOCKED:
response.WriteOCSError(w, r, response.MetaLocked.StatusCode, uRes.GetStatus().GetMessage(), nil)
return
}
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "grpc delete share request failed", err)
return
}
if currentUser, ok := ctxpkg.ContextGetUser(ctx); ok {
h.statCache.RemoveStat(currentUser.Id, share.ResourceId)
}
response.WriteOCSSuccess(w, r, data)
}
func (h *Handler) listUserShares(r *http.Request, filters []*collaboration.Filter) ([]*conversions.ShareData, *rpc.Status, error) {
ctx := r.Context()
log := appctx.GetLogger(ctx)
lsUserSharesRequest := collaboration.ListSharesRequest{
Filters: filters,
}
ocsDataPayload := make([]*conversions.ShareData, 0)
if h.gatewayAddr != "" {
// get a connection to the users share provider
client, err := h.getClient()
if err != nil {
return ocsDataPayload, nil, err
}
// do list shares request. filtered
lsUserSharesResponse, err := client.ListShares(ctx, &lsUserSharesRequest)
if err != nil {
return ocsDataPayload, nil, err
}
if lsUserSharesResponse.Status.Code != rpc.Code_CODE_OK {
return ocsDataPayload, lsUserSharesResponse.Status, nil
}
// build OCS response payload
for _, s := range lsUserSharesResponse.Shares {
data := conversions.CS3Share2ShareData(ctx, s)
info, status, err := h.getResourceInfoByID(ctx, client, s.ResourceId)
if err != nil || status.Code != rpc.Code_CODE_OK {
log.Debug().Interface("share", s).Interface("status", status).Interface("shareData", data).Err(err).Msg("could not stat share, skipping")
continue
}
u := ctxpkg.ContextMustGetUser(ctx)
// check if the user has the permission to list all shares on the resource
if !utils.UserEqual(s.Creator, u.Id) && !info.GetPermissionSet().ListGrants {
log.Debug().Interface("share", s).Interface("user", u).Msg("user has no permission to list all grants and is not the creator of this share")
continue
}
h.addFileInfo(ctx, data, info)
h.mapUserIds(ctx, client, data)
// Filter out a share if ShareWith is not found because the user or group already deleted
if data.ShareWith == "" {
continue
}
log.Debug().Interface("share", s).Interface("info", info).Interface("shareData", data).Msg("mapped")
ocsDataPayload = append(ocsDataPayload, data)
}
if h.listOCMShares {
// include the ocm shares
ocmShares, err := h.listOutcomingFederatedShares(ctx, client, convertToOCMFilters(filters))
if err != nil {
return nil, nil, err
}
ocsDataPayload = append(ocsDataPayload, ocmShares...)
}
}
return ocsDataPayload, nil, nil
}
func convertToOCMFilters(filters []*collaboration.Filter) []*ocmpb.ListOCMSharesRequest_Filter {
ocmfilters := []*ocmpb.ListOCMSharesRequest_Filter{}
for _, f := range filters {
switch v := f.Term.(type) {
case *collaboration.Filter_ResourceId:
ocmfilters = append(ocmfilters, &ocmpb.ListOCMSharesRequest_Filter{
Type: ocmpb.ListOCMSharesRequest_Filter_TYPE_RESOURCE_ID,
Term: &ocmpb.ListOCMSharesRequest_Filter_ResourceId{
ResourceId: v.ResourceId,
},
})
case *collaboration.Filter_Creator:
ocmfilters = append(ocmfilters, &ocmpb.ListOCMSharesRequest_Filter{
Type: ocmpb.ListOCMSharesRequest_Filter_TYPE_CREATOR,
Term: &ocmpb.ListOCMSharesRequest_Filter_Creator{
Creator: v.Creator,
},
})
case *collaboration.Filter_Owner:
ocmfilters = append(ocmfilters, &ocmpb.ListOCMSharesRequest_Filter{
Type: ocmpb.ListOCMSharesRequest_Filter_TYPE_OWNER,
Term: &ocmpb.ListOCMSharesRequest_Filter_Owner{
Owner: v.Owner,
},
})
}
}
return ocmfilters
}
@@ -0,0 +1,238 @@
// Copyright 2018-2021 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 capabilities
import (
"net/http"
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocs/config"
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocs/response"
"github.com/opencloud-eu/reva/v2/pkg/owncloud/ocs"
)
// Handler renders the capability endpoint
type Handler struct {
c ocs.CapabilitiesData
defaultUploadProtocol string
userAgentChunkingMap map[string]string
}
// Init initializes this and any contained handlers
func (h *Handler) Init(c *config.Config) {
h.c = c.Capabilities
h.defaultUploadProtocol = c.DefaultUploadProtocol
h.userAgentChunkingMap = c.UserAgentChunkingMap
// capabilities
if h.c.Capabilities == nil {
h.c.Capabilities = &ocs.Capabilities{}
}
// core
if h.c.Capabilities.Core == nil {
h.c.Capabilities.Core = &ocs.CapabilitiesCore{}
}
if h.c.Capabilities.Core.PollInterval == 0 {
h.c.Capabilities.Core.PollInterval = 60
}
if h.c.Capabilities.Core.WebdavRoot == "" {
h.c.Capabilities.Core.WebdavRoot = "remote.php/webdav"
}
// h.c.Capabilities.Core.SupportURLSigning is boolean
if h.c.Capabilities.Core.Status == nil {
h.c.Capabilities.Core.Status = &ocs.Status{}
}
// h.c.Capabilities.Core.Status.Installed is boolean
// h.c.Capabilities.Core.Status.Maintenance is boolean
// h.c.Capabilities.Core.Status.NeedsDBUpgrade is boolean
if h.c.Capabilities.Core.Status.Version == "" {
h.c.Capabilities.Core.Status.Version = "10.0.11.5" // TODO make build determined
}
if h.c.Capabilities.Core.Status.VersionString == "" {
h.c.Capabilities.Core.Status.VersionString = "10.0.11" // TODO make build determined
}
if h.c.Capabilities.Core.Status.Edition == "" {
h.c.Capabilities.Core.Status.Edition = "community" // TODO make build determined
}
if h.c.Capabilities.Core.Status.ProductName == "" {
h.c.Capabilities.Core.Status.ProductName = "reva" // TODO make build determined
}
if h.c.Capabilities.Core.Status.Product == "" {
h.c.Capabilities.Core.Status.Product = "reva" // TODO make build determined
}
if h.c.Capabilities.Core.Status.Hostname == "" {
h.c.Capabilities.Core.Status.Hostname = "" // TODO get from context?
}
// checksums
if h.c.Capabilities.Checksums == nil {
h.c.Capabilities.Checksums = &ocs.CapabilitiesChecksums{}
}
if h.c.Capabilities.Checksums.SupportedTypes == nil {
h.c.Capabilities.Checksums.SupportedTypes = []string{"SHA256"}
}
if h.c.Capabilities.Checksums.PreferredUploadType == "" {
h.c.Capabilities.Checksums.PreferredUploadType = "SHA1"
}
// files
if h.c.Capabilities.Files == nil {
h.c.Capabilities.Files = &ocs.CapabilitiesFiles{}
}
if h.c.Capabilities.Files.BlacklistedFiles == nil {
h.c.Capabilities.Files.BlacklistedFiles = []string{}
}
// h.c.Capabilities.Files.Undelete is boolean
// h.c.Capabilities.Files.Versioning is boolean
// h.c.Capabilities.Files.Favorites is boolean
if h.c.Capabilities.Files.Archivers == nil {
h.c.Capabilities.Files.Archivers = []*ocs.CapabilitiesArchiver{}
}
if h.c.Capabilities.Files.AppProviders == nil {
h.c.Capabilities.Files.AppProviders = []*ocs.CapabilitiesAppProvider{}
}
// dav
if h.c.Capabilities.Dav == nil {
h.c.Capabilities.Dav = &ocs.CapabilitiesDav{}
}
if h.c.Capabilities.Dav.Trashbin == "" {
h.c.Capabilities.Dav.Trashbin = "1.0"
}
if h.c.Capabilities.Dav.Reports == nil {
h.c.Capabilities.Dav.Reports = []string{}
}
// sharing
if h.c.Capabilities.FilesSharing == nil {
h.c.Capabilities.FilesSharing = &ocs.CapabilitiesFilesSharing{}
}
// h.c.Capabilities.FilesSharing.APIEnabled is boolean
if h.c.Capabilities.FilesSharing.Public == nil {
h.c.Capabilities.FilesSharing.Public = &ocs.CapabilitiesFilesSharingPublic{}
}
// h.c.Capabilities.FilesSharing.IsPublic.Enabled is boolean
h.c.Capabilities.FilesSharing.Public.Enabled = true
if h.c.Capabilities.FilesSharing.Public.Password == nil {
h.c.Capabilities.FilesSharing.Public.Password = &ocs.CapabilitiesFilesSharingPublicPassword{}
}
if h.c.Capabilities.FilesSharing.Public.Password.EnforcedFor == nil {
h.c.Capabilities.FilesSharing.Public.Password.EnforcedFor = &ocs.CapabilitiesFilesSharingPublicPasswordEnforcedFor{}
}
// h.c.Capabilities.FilesSharing.IsPublic.Password.EnforcedFor.ReadOnly is boolean
// h.c.Capabilities.FilesSharing.IsPublic.Password.EnforcedFor.ReadWrite is boolean
// h.c.Capabilities.FilesSharing.IsPublic.Password.EnforcedFor.ReadWriteDelete is boolean
// h.c.Capabilities.FilesSharing.IsPublic.Password.EnforcedFor.UploadOnly is boolean
// h.c.Capabilities.FilesSharing.IsPublic.Password.Enforced is boolean
if h.c.Capabilities.FilesSharing.Public.ExpireDate == nil {
h.c.Capabilities.FilesSharing.Public.ExpireDate = &ocs.CapabilitiesFilesSharingPublicExpireDate{}
}
// h.c.Capabilities.FilesSharing.IsPublic.ExpireDate.Enabled is boolean
// h.c.Capabilities.FilesSharing.IsPublic.SendMail is boolean
// h.c.Capabilities.FilesSharing.IsPublic.SocialShare is boolean
// h.c.Capabilities.FilesSharing.IsPublic.Upload is boolean
// h.c.Capabilities.FilesSharing.IsPublic.Multiple is boolean
// h.c.Capabilities.FilesSharing.IsPublic.SupportsUploadOnly is boolean
if h.c.Capabilities.FilesSharing.User == nil {
h.c.Capabilities.FilesSharing.User = &ocs.CapabilitiesFilesSharingUser{}
}
// h.c.Capabilities.FilesSharing.User.SendMail is boolean
// h.c.Capabilities.FilesSharing.Resharing is boolean
// h.c.Capabilities.FilesSharing.GroupSharing is boolean
// h.c.Capabilities.FilesSharing.SharingRoles is boolean
// h.c.Capabilities.FilesSharing.AutoAcceptShare is boolean
// h.c.Capabilities.FilesSharing.ShareWithGroupMembersOnly is boolean
// h.c.Capabilities.FilesSharing.ShareWithMembershipGroupsOnly is boolean
if h.c.Capabilities.FilesSharing.UserEnumeration == nil {
h.c.Capabilities.FilesSharing.UserEnumeration = &ocs.CapabilitiesFilesSharingUserEnumeration{}
}
// h.c.Capabilities.FilesSharing.UserEnumeration.Enabled is boolean
// h.c.Capabilities.FilesSharing.UserEnumeration.GroupMembersOnly is boolean
if h.c.Capabilities.FilesSharing.DefaultPermissions == 0 {
h.c.Capabilities.FilesSharing.DefaultPermissions = 31
}
if h.c.Capabilities.FilesSharing.Federation == nil {
h.c.Capabilities.FilesSharing.Federation = &ocs.CapabilitiesFilesSharingFederation{}
}
// h.c.Capabilities.FilesSharing.Federation.Outgoing is boolean
// h.c.Capabilities.FilesSharing.Federation.Incoming is boolean
if h.c.Capabilities.FilesSharing.SearchMinLength == 0 {
h.c.Capabilities.FilesSharing.SearchMinLength = 2
}
// notifications
// if h.c.Capabilities.Notifications == nil {
// h.c.Capabilities.Notifications = &ocs.CapabilitiesNotifications{}
// }
// if h.c.Capabilities.Notifications.Endpoints == nil {
// h.c.Capabilities.Notifications.Endpoints = []string{"list", "get", "delete"}
// }
// version
if h.c.Version == nil {
h.c.Version = &ocs.Version{
// TODO get from build env
Major: 10,
Minor: 0,
Micro: 11,
String: "10.0.11",
Edition: "community",
Product: "reva",
ProductVersion: "",
}
}
// upload protocol-specific details
setCapabilitiesForChunkProtocol(chunkProtocol(h.defaultUploadProtocol), &h.c)
}
// Handler renders the capabilities
func (h *Handler) GetCapabilities(w http.ResponseWriter, r *http.Request) {
c := h.getCapabilitiesForUserAgent(r.UserAgent())
response.WriteOCSSuccess(w, r, c)
}
@@ -0,0 +1,79 @@
// Copyright 2018-2021 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 capabilities
import (
"strings"
"github.com/opencloud-eu/reva/v2/pkg/owncloud/ocs"
)
type chunkProtocol string
var (
chunkV1 chunkProtocol = "v1"
chunkNG chunkProtocol = "ng"
chunkTUS chunkProtocol = "tus"
)
func (h *Handler) getCapabilitiesForUserAgent(userAgent string) ocs.CapabilitiesData {
if userAgent != "" {
for k, v := range h.userAgentChunkingMap {
// we could also use a regexp for pattern matching
if strings.Contains(userAgent, k) {
// Creating a copy of the capabilities struct is less expensive than taking a lock
c := h.c
setCapabilitiesForChunkProtocol(chunkProtocol(v), &c)
return c
}
}
}
return h.c
}
func setCapabilitiesForChunkProtocol(cp chunkProtocol, c *ocs.CapabilitiesData) {
switch cp {
case chunkV1:
// 2.7+ will use Chunking V1 if "capabilities > files > bigfilechunking" is "true" AND "capabilities > dav > chunking" is not there
c.Capabilities.Files.BigFileChunking = true
c.Capabilities.Dav = nil
c.Capabilities.Files.TusSupport = nil
case chunkNG:
// 2.7+ will use Chunking NG if "capabilities > files > bigfilechunking" is "true" AND "capabilities > dav > chunking" = 1.0
c.Capabilities.Files.BigFileChunking = true
c.Capabilities.Dav.Chunking = "1.0"
c.Capabilities.Files.TusSupport = nil
case chunkTUS:
// 2.7+ will use TUS if "capabilities > files > bigfilechunking" is "false" AND "capabilities > dav > chunking" = "" AND "capabilities > files > tus_support" has proper entries.
c.Capabilities.Files.BigFileChunking = false
c.Capabilities.Dav.Chunking = ""
// TODO: infer from various TUS handlers from all known storages
// until now we take the manually configured tus options
// c.Capabilities.Files.TusSupport = &data.CapabilitiesFilesTusSupport{
// Version: "1.0.0",
// Resumable: "1.0.0",
// Extension: "creation,creation-with-upload",
// MaxChunkSize: 0,
// HTTPMethodOverride: "",
// }
}
}
@@ -0,0 +1,59 @@
// Copyright 2018-2021 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 user
import (
"fmt"
"net/http"
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocs/response"
"github.com/opencloud-eu/reva/v2/pkg/conversions"
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
)
// The Handler renders the user endpoint
type Handler struct {
}
// GetSelf handles GET requests on /cloud/user
func (h *Handler) GetSelf(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
// TODO move user to handler parameter?
u, ok := ctxpkg.ContextGetUser(ctx)
if !ok {
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "missing user in context", fmt.Errorf("missing user in context"))
return
}
response.WriteOCSSuccess(w, r, &User{
ID: u.Username,
DisplayName: u.DisplayName,
Email: u.Mail,
UserType: conversions.UserTypeString(u.Id.Type),
})
}
// User holds user data
type User struct {
ID string `json:"id" xml:"id"` // UserID in ocs is the owncloud internal username
DisplayName string `json:"display-name" xml:"display-name"` // is used in ocs/v(1|2).php/cloud/user - yes this is different from the users endpoint
Email string `json:"email" xml:"email"`
UserType string `json:"user-type" xml:"user-type"`
}
@@ -0,0 +1,230 @@
// Copyright 2018-2021 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 users
import (
"context"
"fmt"
"net/http"
"net/url"
"strconv"
cs3gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
cs3identity "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
cs3rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
cs3storage "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/go-chi/chi/v5"
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocs/config"
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocs/response"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
"github.com/opencloud-eu/reva/v2/pkg/conversions"
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/opencloud-eu/reva/v2/pkg/utils"
)
// Handler renders user data for the user id given in the url path
type Handler struct {
gatewayAddr string
}
// Init initializes this and any contained handlers
func (h *Handler) Init(c *config.Config) {
h.gatewayAddr = c.GatewaySvc
}
// GetGroups handles GET requests on /cloud/users/groups
// TODO: implement
func (h *Handler) GetGroups(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
user := chi.URLParam(r, "userid")
// FIXME use ldap to fetch user info
u, ok := ctxpkg.ContextGetUser(ctx)
if !ok {
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "missing user in context", fmt.Errorf("missing user in context"))
return
}
if user != u.Username {
// FIXME allow fetching other users info? only for admins
response.WriteOCSError(w, r, http.StatusForbidden, "user id mismatch", fmt.Errorf("%s tried to access %s user info endpoint", u.Id.OpaqueId, user))
return
}
response.WriteOCSSuccess(w, r, &Groups{Groups: u.Groups})
}
// Quota holds quota information
type Quota struct {
Free int64 `json:"free,omitempty" xml:"free,omitempty"`
Used int64 `json:"used,omitempty" xml:"used,omitempty"`
Total int64 `json:"total,omitempty" xml:"total,omitempty"`
Relative float32 `json:"relative,omitempty" xml:"relative,omitempty"`
Definition string `json:"definition,omitempty" xml:"definition,omitempty"`
}
// User holds user data
type User struct {
Enabled string `json:"enabled" xml:"enabled"`
Quota *Quota `json:"quota,omitempty" xml:"quota,omitempty"`
Email string `json:"email" xml:"email"`
DisplayName string `json:"displayname" xml:"displayname"` // is used in ocs/v(1|2).php/cloud/users/{username} - yes this is different from the /user endpoint
UserType string `json:"user-type" xml:"user-type"`
UIDNumber int64 `json:"uidnumber,omitempty" xml:"uidnumber,omitempty"`
GIDNumber int64 `json:"gidnumber,omitempty" xml:"gidnumber,omitempty"`
// FIXME home should never be exposed ... even in oc 10, well only the admin can call this endpoint ...
// Home string `json:"home" xml:"home"`
TwoFactorAuthEnabled bool `json:"two_factor_auth_enabled" xml:"two_factor_auth_enabled"`
LastLogin int64 `json:"last_login" xml:"last_login"`
}
// Groups holds group data
type Groups struct {
Groups []string `json:"groups" xml:"groups>element"`
}
// GetUsers handles GET requests on /cloud/users
// Only allow self-read currently. TODO: List Users and Get on other users (both require
// administrative privileges)
func (h *Handler) GetUsers(w http.ResponseWriter, r *http.Request) {
userid := chi.URLParam(r, "userid")
userid, err := url.PathUnescape(userid)
if err != nil {
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "could not unescape username", err)
return
}
currentUser, ok := ctxpkg.ContextGetUser(r.Context())
if !ok {
response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "missing user in context", fmt.Errorf("missing user in context"))
return
}
var user *cs3identity.User
switch {
case userid == "":
response.WriteOCSError(w, r, response.MetaBadRequest.StatusCode, "missing username", fmt.Errorf("missing username"))
return
case userid == currentUser.Username:
user = currentUser
default:
// FIXME allow fetching other users info? only for admins
response.WriteOCSError(w, r, http.StatusForbidden, "user id mismatch", fmt.Errorf("%s tried to access %s user info endpoint", currentUser.Id.OpaqueId, user))
return
}
d := &User{
Enabled: "true", // TODO include in response only when admin?
DisplayName: user.DisplayName,
Email: user.Mail,
UserType: conversions.UserTypeString(user.Id.Type),
Quota: &Quota{},
}
// TODO how do we fill lastlogin of a user when another user (with the necessary permissions) looks up the user?
// TODO someone needs to fill last-login
if lastLogin := utils.ReadPlainFromOpaque(user.Opaque, "last-login"); lastLogin != "" {
d.LastLogin, _ = strconv.ParseInt(lastLogin, 10, 64)
}
// lightweight and federated users don't have access to their storage space
if currentUser.Id.Type != cs3identity.UserType_USER_TYPE_LIGHTWEIGHT && currentUser.Id.Type != cs3identity.UserType_USER_TYPE_FEDERATED {
h.fillPersonalQuota(r.Context(), d, user)
}
response.WriteOCSSuccess(w, r, d)
}
func (h Handler) fillPersonalQuota(ctx context.Context, d *User, u *cs3identity.User) {
sublog := appctx.GetLogger(ctx)
gc, err := pool.GetGatewayServiceClient(h.gatewayAddr)
if err != nil {
sublog.Error().Err(err).Msg("error getting gateway client")
return
}
res, err := gc.ListStorageSpaces(ctx, &cs3storage.ListStorageSpacesRequest{
Filters: []*cs3storage.ListStorageSpacesRequest_Filter{
{
Type: cs3storage.ListStorageSpacesRequest_Filter_TYPE_OWNER,
Term: &cs3storage.ListStorageSpacesRequest_Filter_Owner{
Owner: u.Id,
},
},
{
Type: cs3storage.ListStorageSpacesRequest_Filter_TYPE_SPACE_TYPE,
Term: &cs3storage.ListStorageSpacesRequest_Filter_SpaceType{
SpaceType: "personal",
},
},
},
})
if err != nil {
sublog.Error().Err(err).Msg("error calling ListStorageSpaces")
return
}
if res.Status.Code != cs3rpc.Code_CODE_OK {
return
}
if len(res.StorageSpaces) == 0 {
sublog.Error().Err(err).Msg("list spaces returned empty list")
return
}
getQuotaRes, err := gc.GetQuota(ctx, &cs3gateway.GetQuotaRequest{Ref: &cs3storage.Reference{
ResourceId: res.StorageSpaces[0].Root,
Path: ".",
}})
if err != nil {
sublog.Error().Err(err).Msg("error calling GetQuota")
return
}
if res.Status.Code != cs3rpc.Code_CODE_OK {
sublog.Debug().Interface("status", res.Status).Msg("GetQuota returned non OK result")
return
}
total := getQuotaRes.TotalBytes
used := getQuotaRes.UsedBytes
d.Quota = &Quota{
Used: int64(used),
// TODO support negative values or flags for the quota to carry special meaning: -1 = uncalculated, -2 = unknown, -3 = unlimited
// for now we can only report total and used
Total: int64(total),
// we cannot differentiate between `default` or a human readable `1 GB` definition.
// The web UI can create a human readable string from the actual total if it is set. Otherwise it has to leave out relative and total anyway.
// Definition: "default",
}
if raw := utils.ReadPlainFromOpaque(getQuotaRes.Opaque, "remaining"); raw != "" {
d.Quota.Free, _ = strconv.ParseInt(raw, 10, 64)
}
// only calculate free and relative when total is available
if total > 0 {
d.Quota.Free = int64(total - used)
d.Quota.Relative = float32((float64(used) / float64(total)) * 100.0)
} else {
d.Quota.Definition = "none" // this indicates no quota / unlimited to the ui
}
}
@@ -0,0 +1,64 @@
// Copyright 2018-2021 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 ocs
import (
"net/http"
"net/url"
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocs/config"
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocs/data"
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocs/response"
)
// Handler renders the config endpoint
type Handler struct {
c data.ConfigData
}
// Init initializes this and any contained handlers
func (h *Handler) Init(c *config.Config) {
h.c = c.Config
// config
if h.c.Version == "" {
h.c.Version = "1.7"
}
if h.c.Website == "" {
h.c.Website = "reva"
}
if h.c.Host == "" {
h.c.Host = "" // TODO get from context?
}
if h.c.Contact == "" {
h.c.Contact = ""
}
if h.c.SSL == "" {
h.c.SSL = "false" // TODO get from context?
}
// ensure that host has no protocol
if url, err := url.Parse(h.c.Host); err == nil {
h.c.Host = url.Host + url.Path
}
}
// Handler renders the config
func (h *Handler) GetConfig(w http.ResponseWriter, r *http.Request) {
response.WriteOCSSuccess(w, r, h.c)
}
@@ -0,0 +1,178 @@
// Copyright 2018-2021 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 ocs
import (
"net/http"
"github.com/go-chi/chi/v5"
"github.com/jellydator/ttlcache/v2"
"github.com/mitchellh/mapstructure"
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocs/config"
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocs/handlers/apps/sharing/sharees"
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares"
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocs/handlers/cloud/capabilities"
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocs/handlers/cloud/user"
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocs/handlers/cloud/users"
configHandler "github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocs/handlers/config"
"github.com/opencloud-eu/reva/v2/internal/http/services/owncloud/ocs/response"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
"github.com/opencloud-eu/reva/v2/pkg/rhttp/global"
"github.com/rs/zerolog"
)
func init() {
global.Register("ocs", New)
}
type svc struct {
c *config.Config
router *chi.Mux
warmupCacheTracker *ttlcache.Cache
}
// New initializes the service
func New(m map[string]interface{}, log *zerolog.Logger) (global.Service, error) {
conf := &config.Config{}
if err := mapstructure.Decode(m, conf); err != nil {
return nil, err
}
conf.Init()
r := chi.NewRouter()
s := &svc{
c: conf,
router: r,
}
if err := s.routerInit(log); err != nil {
return nil, err
}
if conf.CacheWarmupDriver == "first-request" && conf.StatCacheConfig.Store != "noop" {
s.warmupCacheTracker = ttlcache.NewCache()
_ = s.warmupCacheTracker.SetTTL(conf.StatCacheConfig.TTL)
}
return s, nil
}
func (s *svc) Prefix() string {
return s.c.Prefix
}
func (s *svc) Close() error {
return nil
}
func (s *svc) Unprotected() []string {
return []string{
"/v1.php/config",
"/v2.php/config",
"/v1.php/apps/files_sharing/api/v1/tokeninfo/unprotected",
"/v2.php/apps/files_sharing/api/v1/tokeninfo/unprotected",
"/v1.php/cloud/capabilities",
"/v2.php/cloud/capabilities",
}
}
func (s *svc) routerInit(log *zerolog.Logger) error {
capabilitiesHandler := new(capabilities.Handler)
userHandler := new(user.Handler)
usersHandler := new(users.Handler)
configHandler := new(configHandler.Handler)
sharesHandler := new(shares.Handler)
shareesHandler := new(sharees.Handler)
capabilitiesHandler.Init(s.c)
usersHandler.Init(s.c)
configHandler.Init(s.c)
err := sharesHandler.Init(s.c)
if err != nil {
log.Fatal().Msg(err.Error())
}
shareesHandler.Init(s.c)
s.router.Route("/v{version:(1|2)}.php", func(r chi.Router) {
r.Use(response.VersionCtx)
r.Route("/apps/files_sharing/api/v1", func(r chi.Router) {
r.Route("/shares", func(r chi.Router) {
r.Get("/", sharesHandler.ListShares)
r.Options("/", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
r.Post("/", sharesHandler.CreateShare)
r.Route("/pending/{shareid}", func(r chi.Router) {
r.Post("/", sharesHandler.AcceptReceivedShare)
r.Delete("/", sharesHandler.RejectReceivedShare)
r.Put("/", sharesHandler.UpdateReceivedShare)
})
r.Route("/remote_shares", func(r chi.Router) {
r.Get("/", sharesHandler.ListFederatedShares)
r.Get("/{shareid}", sharesHandler.GetFederatedShare)
})
r.Get("/{shareid}", sharesHandler.GetShare)
r.Put("/{shareid}", sharesHandler.UpdateShare)
r.Delete("/{shareid}", sharesHandler.RemoveShare)
})
r.Get("/sharees", shareesHandler.FindSharees)
r.Route("/tokeninfo", func(r chi.Router) {
r.Get("/protected/{tkn}", shareesHandler.TokenInfo(true))
r.Get("/unprotected/{tkn}", shareesHandler.TokenInfo(false))
})
})
// placeholder for notifications
r.Get("/apps/notifications/api/v1/notifications", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
r.Get("/config", configHandler.GetConfig)
r.Route("/cloud", func(r chi.Router) {
r.Get("/capabilities", capabilitiesHandler.GetCapabilities)
r.Get("/user", userHandler.GetSelf)
r.Route("/users", func(r chi.Router) {
r.Get("/{userid}", usersHandler.GetUsers)
r.Get("/{userid}/groups", usersHandler.GetGroups)
})
})
})
_ = chi.Walk(s.router, func(method string, route string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error {
log.Debug().Str("service", "ocs").Str("method", method).Str("route", route).Int("middlewares", len(middlewares)).Msg("serving endpoint")
return nil
})
return nil
}
func (s *svc) Handler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log := appctx.GetLogger(r.Context())
log.Debug().Str("path", r.URL.Path).Msg("ocs routing")
// Warmup the share cache for the user
go s.cacheWarmup(w, r)
// unset raw path, otherwise chi uses it to route and then fails to match percent encoded path segments
r.URL.RawPath = ""
s.router.ServeHTTP(w, r)
})
}
@@ -0,0 +1,308 @@
// Copyright 2018-2021 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 response
import (
"bytes"
"context"
"encoding/json"
"encoding/xml"
"net/http"
"reflect"
"github.com/go-chi/chi/v5"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
)
type key int
const (
apiVersionKey key = 1
)
var (
defaultStatusCodeMapper = OcsV2StatusCodes
)
// Response is the top level response structure
type Response struct {
OCS *Payload `json:"ocs"`
}
// Payload combines response metadata and data
type Payload struct {
XMLName struct{} `json:"-" xml:"ocs"`
Meta Meta `json:"meta" xml:"meta"`
Data interface{} `json:"data,omitempty" xml:"data,omitempty"`
}
var (
elementStartElement = xml.StartElement{Name: xml.Name{Local: "element"}}
metaStartElement = xml.StartElement{Name: xml.Name{Local: "meta"}}
ocsName = xml.Name{Local: "ocs"}
dataName = xml.Name{Local: "data"}
)
// MarshalXML handles ocs specific wrapping of array members in 'element' tags for the data
func (p Payload) MarshalXML(e *xml.Encoder, start xml.StartElement) (err error) {
// first the easy part
// use ocs as the surrounding tag
start.Name = ocsName
if err = e.EncodeToken(start); err != nil {
return
}
// encode the meta tag
if err = e.EncodeElement(p.Meta, metaStartElement); err != nil {
return
}
// we need to use reflection to determine if p.Data is an array or a slice
rt := reflect.TypeOf(p.Data)
if rt != nil && (rt.Kind() == reflect.Array || rt.Kind() == reflect.Slice) {
// this is how to wrap the data elements in their own <element> tag
v := reflect.ValueOf(p.Data)
if err = e.EncodeToken(xml.StartElement{Name: dataName}); err != nil {
return
}
for i := 0; i < v.Len(); i++ {
if err = e.EncodeElement(v.Index(i).Interface(), elementStartElement); err != nil {
return
}
}
if err = e.EncodeToken(xml.EndElement{Name: dataName}); err != nil {
return
}
} else if err = e.EncodeElement(p.Data, xml.StartElement{Name: dataName}); err != nil {
return
}
// write the closing <ocs> tag
if err = e.EncodeToken(xml.EndElement{Name: start.Name}); err != nil {
return
}
return
}
// Meta holds response metadata
type Meta struct {
Status string `json:"status" xml:"status"`
StatusCode int `json:"statuscode" xml:"statuscode"`
Message string `json:"message" xml:"message"`
TotalItems string `json:"totalitems,omitempty" xml:"totalitems,omitempty"`
ItemsPerPage string `json:"itemsperpage,omitempty" xml:"itemsperpage,omitempty"`
}
// MetaOK is the default ok response
var MetaOK = Meta{Status: "ok", StatusCode: 100, Message: "OK"}
// MetaFailure is a failure response with code 101
var MetaFailure = Meta{Status: "", StatusCode: 101, Message: "Failure"}
// MetaInvalidInput is an error response with code 102
var MetaInvalidInput = Meta{Status: "", StatusCode: 102, Message: "Invalid Input"}
// MetaForbidden is an error response with code 104
var MetaForbidden = Meta{Status: "", StatusCode: 104, Message: "Forbidden"}
// MetaBadRequest is used for unknown errors
var MetaBadRequest = Meta{Status: "error", StatusCode: 400, Message: "Bad Request"}
// MetaPathNotFound is returned when trying to share not existing resources
var MetaPathNotFound = Meta{Status: "failure", StatusCode: 404, Message: MessagePathNotFound}
// MetaLocked is returned when trying to share not existing resources
var MetaLocked = Meta{Status: "failure", StatusCode: 423, Message: "The file is locked"}
// MetaServerError is returned on server errors
var MetaServerError = Meta{Status: "error", StatusCode: 996, Message: "Server Error"}
// MetaUnauthorized is returned on unauthorized requests
var MetaUnauthorized = Meta{Status: "error", StatusCode: 997, Message: "Unauthorised"}
// MetaNotFound is returned when trying to access not existing resources
var MetaNotFound = Meta{Status: "error", StatusCode: 998, Message: "Not Found"}
// MetaUnknownError is used for unknown errors
var MetaUnknownError = Meta{Status: "error", StatusCode: 999, Message: "Unknown Error"}
// MessageUserNotFound is used when a user can not be found
var MessageUserNotFound = "The requested user could not be found"
// MessageGroupNotFound is used when a group can not be found
var MessageGroupNotFound = "The requested group could not be found"
// MessagePathNotFound is used when a file or folder can not be found
var MessagePathNotFound = "Wrong path, file/folder doesn't exist"
// MessageShareExists is used when a user tries to create a new share for the same user
var MessageShareExists = "A share for the recipient already exists"
// MessageLockedForSharing is used when a user tries to create a new share until the file is in use by at least one user
var MessageLockedForSharing = "The file is locked until the file is in use by at least one user"
// WriteOCSSuccess handles writing successful ocs response data
func WriteOCSSuccess(w http.ResponseWriter, r *http.Request, d interface{}) {
WriteOCSData(w, r, MetaOK, d, nil)
}
// WriteOCSError handles writing error ocs responses
func WriteOCSError(w http.ResponseWriter, r *http.Request, c int, m string, err error) {
WriteOCSData(w, r, Meta{Status: "error", StatusCode: c, Message: m}, nil, err)
}
// WriteOCSData handles writing ocs data in json and xml
func WriteOCSData(w http.ResponseWriter, r *http.Request, m Meta, d interface{}, err error) {
WriteOCSResponse(w, r, Response{
OCS: &Payload{
Meta: m,
Data: d,
},
}, err)
}
// WriteOCSResponse handles writing ocs responses in json and xml
func WriteOCSResponse(w http.ResponseWriter, r *http.Request, res Response, err error) {
if err != nil {
appctx.GetLogger(r.Context()).
Debug().
Err(err).
Str("ocs_msg", res.OCS.Meta.Message).
Msg("writing ocs error response")
}
version := APIVersion(r.Context())
m := statusCodeMapper(version)
statusCode := m(res.OCS.Meta)
if version == "2" && statusCode == http.StatusOK {
res.OCS.Meta.StatusCode = statusCode
}
var encoder func(Response) ([]byte, error)
if r.URL.Query().Get("format") == "json" {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
encoder = encodeJSON
} else {
w.Header().Set("Content-Type", "text/xml; charset=utf-8")
encoder = encodeXML
}
w.WriteHeader(statusCode)
encoded, err := encoder(res)
if err != nil {
appctx.GetLogger(r.Context()).Error().Err(err).Msg("error encoding ocs response")
w.WriteHeader(http.StatusInternalServerError)
return
}
_, err = w.Write(encoded)
if err != nil {
appctx.GetLogger(r.Context()).Error().Err(err).Msg("error writing ocs response")
w.WriteHeader(http.StatusInternalServerError)
}
}
func encodeXML(res Response) ([]byte, error) {
marshalled, err := xml.Marshal(res.OCS)
if err != nil {
return nil, err
}
b := new(bytes.Buffer)
b.WriteString(xml.Header)
b.Write(marshalled)
return b.Bytes(), nil
}
func encodeJSON(res Response) ([]byte, error) {
return json.Marshal(res)
}
// OcsV1StatusCodes returns the http status codes for the OCS API v1.
func OcsV1StatusCodes(meta Meta) int {
return http.StatusOK
}
// OcsV2StatusCodes maps the OCS codes to http status codes for the ocs API v2.
func OcsV2StatusCodes(meta Meta) int {
sc := meta.StatusCode
switch sc {
case MetaNotFound.StatusCode:
return http.StatusNotFound
case MetaUnknownError.StatusCode:
fallthrough
case MetaServerError.StatusCode:
return http.StatusInternalServerError
case MetaUnauthorized.StatusCode:
return http.StatusUnauthorized
case MetaOK.StatusCode:
meta.StatusCode = http.StatusOK
return http.StatusOK
case MetaForbidden.StatusCode:
return http.StatusForbidden
}
// any 2xx, 4xx and 5xx will be used as is
if sc >= 200 && sc < 600 {
return sc
}
// any error codes > 100 are treated as client errors
if sc > 100 && sc < 200 {
return http.StatusBadRequest
}
// TODO change this status code?
return http.StatusOK
}
// WithAPIVersion puts the api version in the context.
func VersionCtx(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
version := chi.URLParam(r, "version")
if version == "" {
WriteOCSError(w, r, MetaBadRequest.StatusCode, "unknown ocs api version", nil)
return
}
w.Header().Set("Ocs-Api-Version", version)
// store version in context so handlers can access it
ctx := context.WithValue(r.Context(), apiVersionKey, version)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
// APIVersion retrieves the api version from the context.
func APIVersion(ctx context.Context) string {
value := ctx.Value(apiVersionKey)
if value != nil {
return value.(string)
}
return ""
}
func statusCodeMapper(version string) func(Meta) int {
var mapper func(Meta) int
switch version {
case "1":
mapper = OcsV1StatusCodes
case "2":
mapper = OcsV2StatusCodes
default:
mapper = defaultStatusCodeMapper
}
return mapper
}
@@ -0,0 +1,218 @@
// Copyright 2018-2021 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 preferences
import (
"encoding/json"
"net/http"
preferences "github.com/cs3org/go-cs3apis/cs3/preferences/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
"github.com/go-chi/chi/v5"
"github.com/mitchellh/mapstructure"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/opencloud-eu/reva/v2/pkg/rhttp/global"
"github.com/opencloud-eu/reva/v2/pkg/sharedconf"
"github.com/rs/zerolog"
)
func init() {
global.Register("preferences", New)
}
// Config holds the config options that for the preferences HTTP service
type Config struct {
Prefix string `mapstructure:"prefix"`
GatewaySvc string `mapstructure:"gatewaysvc"`
}
func (c *Config) init() {
if c.Prefix == "" {
c.Prefix = "preferences"
}
c.GatewaySvc = sharedconf.GetGatewaySVC(c.GatewaySvc)
}
type svc struct {
conf *Config
router *chi.Mux
}
// New returns a new ocmd object
func New(m map[string]interface{}, log *zerolog.Logger) (global.Service, error) {
conf := &Config{}
if err := mapstructure.Decode(m, conf); err != nil {
return nil, err
}
conf.init()
r := chi.NewRouter()
s := &svc{
conf: conf,
router: r,
}
if err := s.routerInit(log); err != nil {
return nil, err
}
return s, nil
}
func (s *svc) routerInit(log *zerolog.Logger) error {
s.router.Get("/", s.handleGet)
s.router.Post("/", s.handlePost)
_ = chi.Walk(s.router, func(method string, route string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error {
log.Debug().Str("service", "preferences").Str("method", method).Str("route", route).Int("middlewares", len(middlewares)).Msg("serving endpoint")
return nil
})
return nil
}
// Close performs cleanup.
func (s *svc) Close() error {
return nil
}
func (s *svc) Prefix() string {
return s.conf.Prefix
}
func (s *svc) Unprotected() []string {
return []string{}
}
func (s *svc) Handler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
s.router.ServeHTTP(w, r)
})
}
func (s *svc) handleGet(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
log := appctx.GetLogger(ctx)
key := r.URL.Query().Get("key")
ns := r.URL.Query().Get("ns")
if key == "" || ns == "" {
w.WriteHeader(http.StatusBadRequest)
if _, err := w.Write([]byte("key or namespace query missing")); err != nil {
log.Error().Err(err).Msg("error writing to response")
w.WriteHeader(http.StatusInternalServerError)
}
return
}
client, err := pool.GetGatewayServiceClient(s.conf.GatewaySvc)
if err != nil {
log.Error().Err(err).Msg("error getting grpc gateway client")
w.WriteHeader(http.StatusInternalServerError)
return
}
res, err := client.GetKey(ctx, &preferences.GetKeyRequest{
Key: &preferences.PreferenceKey{
Namespace: ns,
Key: key,
},
})
if err != nil {
log.Error().Err(err).Msg("error retrieving key")
w.WriteHeader(http.StatusInternalServerError)
return
}
if res.Status.Code != rpc.Code_CODE_OK {
if res.Status.Code == rpc.Code_CODE_NOT_FOUND {
w.WriteHeader(http.StatusNotFound)
} else {
w.WriteHeader(http.StatusInternalServerError)
}
log.Error().Interface("status", res.Status).Msg("error retrieving key")
return
}
js, err := json.Marshal(map[string]interface{}{
"namespace": ns,
"key": key,
"value": res.Val,
})
if err != nil {
log.Error().Err(err).Msg("error marshalling response")
w.WriteHeader(http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
if _, err = w.Write(js); err != nil {
log.Error().Err(err).Msg("error writing JSON response")
w.WriteHeader(http.StatusInternalServerError)
return
}
}
func (s *svc) handlePost(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
log := appctx.GetLogger(ctx)
key := r.FormValue("key")
ns := r.FormValue("ns")
val := r.FormValue("value")
if key == "" || ns == "" || val == "" {
w.WriteHeader(http.StatusBadRequest)
if _, err := w.Write([]byte("key, namespace or value parameter missing")); err != nil {
log.Error().Err(err).Msg("error writing to response")
w.WriteHeader(http.StatusInternalServerError)
}
return
}
client, err := pool.GetGatewayServiceClient(s.conf.GatewaySvc)
if err != nil {
log.Error().Err(err).Msg("error getting grpc gateway client")
w.WriteHeader(http.StatusInternalServerError)
return
}
res, err := client.SetKey(ctx, &preferences.SetKeyRequest{
Key: &preferences.PreferenceKey{
Namespace: ns,
Key: key,
},
Val: val,
})
if err != nil {
log.Error().Err(err).Msg("error setting key")
w.WriteHeader(http.StatusInternalServerError)
return
}
if res.Status.Code != rpc.Code_CODE_OK {
w.WriteHeader(http.StatusInternalServerError)
log.Error().Interface("status", res.Status).Msg("error setting key")
return
}
}
@@ -0,0 +1,87 @@
// Copyright 2018-2021 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 prometheus
import (
"net/http"
"contrib.go.opencensus.io/exporter/prometheus"
"github.com/mitchellh/mapstructure"
"github.com/pkg/errors"
"github.com/rs/zerolog"
"go.opencensus.io/stats/view"
"github.com/opencloud-eu/reva/v2/pkg/rhttp/global"
)
func init() {
global.Register("prometheus", New)
}
// New returns a new prometheus service
func New(m map[string]interface{}, log *zerolog.Logger) (global.Service, error) {
conf := &config{}
if err := mapstructure.Decode(m, conf); err != nil {
return nil, err
}
conf.init()
pe, err := prometheus.NewExporter(prometheus.Options{
Namespace: "revad",
})
if err != nil {
return nil, errors.Wrap(err, "prometheus: error creating exporter")
}
view.RegisterExporter(pe)
return &svc{prefix: conf.Prefix, h: pe}, nil
}
type config struct {
Prefix string `mapstructure:"prefix"`
}
func (c *config) init() {
if c.Prefix == "" {
c.Prefix = "metrics"
}
}
type svc struct {
prefix string
h http.Handler
}
func (s *svc) Prefix() string {
return s.prefix
}
func (s *svc) Handler() http.Handler {
return s.h
}
func (s *svc) Close() error {
return nil
}
func (s *svc) Unprotected() []string {
// TODO(labkode): all prometheus endpoints are public?
return []string{"/"}
}
@@ -0,0 +1,84 @@
// Copyright 2018-2023 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 reqres
import (
"encoding/json"
"net/http"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
)
// APIErrorCode stores the type of error encountered.
type APIErrorCode string
// The various types of errors that can be expected to occur.
const (
APIErrorNotFound APIErrorCode = "RESOURCE_NOT_FOUND"
APIErrorUnauthenticated APIErrorCode = "UNAUTHENTICATED"
APIErrorUntrustedService APIErrorCode = "UNTRUSTED_SERVICE"
APIErrorUnimplemented APIErrorCode = "FUNCTION_NOT_IMPLEMENTED"
APIErrorInvalidParameter APIErrorCode = "INVALID_PARAMETER"
APIErrorProviderError APIErrorCode = "PROVIDER_ERROR"
APIErrorAlreadyExist APIErrorCode = "ALREADY_EXIST"
APIErrorServerError APIErrorCode = "SERVER_ERROR"
)
// APIErrorCodeMapping stores the HTTP error code mapping for various APIErrorCodes.
var APIErrorCodeMapping = map[APIErrorCode]int{
APIErrorNotFound: http.StatusNotFound,
APIErrorUnauthenticated: http.StatusUnauthorized,
APIErrorUntrustedService: http.StatusForbidden,
APIErrorUnimplemented: http.StatusNotImplemented,
APIErrorInvalidParameter: http.StatusBadRequest,
APIErrorProviderError: http.StatusBadGateway,
APIErrorAlreadyExist: http.StatusConflict,
APIErrorServerError: http.StatusInternalServerError,
}
// APIError encompasses the error type and message.
type APIError struct {
Code APIErrorCode `json:"code"`
Message string `json:"message"`
}
// WriteError handles writing error responses.
func WriteError(w http.ResponseWriter, r *http.Request, code APIErrorCode, message string, e error) {
if e != nil {
appctx.GetLogger(r.Context()).Error().Err(e).Msg(message)
}
var encoded []byte
var err error
w.Header().Set("Content-Type", "application/json")
encoded, err = json.MarshalIndent(APIError{Code: code, Message: message}, "", " ")
if err != nil {
appctx.GetLogger(r.Context()).Error().Err(err).Msg("error encoding response")
w.WriteHeader(http.StatusInternalServerError)
return
}
w.WriteHeader(APIErrorCodeMapping[code])
_, err = w.Write(encoded)
if err != nil {
appctx.GetLogger(r.Context()).Error().Err(err).Msg("error writing response")
w.WriteHeader(http.StatusInternalServerError)
}
}
@@ -0,0 +1,124 @@
// Copyright 2018-2021 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 reverseproxy
import (
"encoding/json"
"net/http"
"net/http/httputil"
"net/url"
"os"
"github.com/go-chi/chi/v5"
"github.com/mitchellh/mapstructure"
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
"github.com/opencloud-eu/reva/v2/pkg/rhttp/global"
"github.com/rs/zerolog"
)
func init() {
global.Register("reverseproxy", New)
}
type proxyRule struct {
Endpoint string `mapstructure:"endpoint" json:"endpoint"`
Backend string `mapstructure:"backend" json:"backend"`
}
type config struct {
ProxyRulesJSON string `mapstructure:"proxy_rules_json"`
}
func (c *config) init() {
if c.ProxyRulesJSON == "" {
c.ProxyRulesJSON = "/etc/revad/proxy_rules.json"
}
}
type svc struct {
router *chi.Mux
}
// New returns an instance of the reverse proxy service
func New(m map[string]interface{}, log *zerolog.Logger) (global.Service, error) {
conf := &config{}
if err := mapstructure.Decode(m, conf); err != nil {
return nil, err
}
conf.init()
f, err := os.ReadFile(conf.ProxyRulesJSON)
if err != nil {
return nil, err
}
var rules []proxyRule
err = json.Unmarshal(f, &rules)
if err != nil {
return nil, err
}
r := chi.NewRouter()
for _, rule := range rules {
remote, err := url.Parse(rule.Backend)
if err != nil {
// Skip the rule if the backend is not a valid URL
continue
}
proxy := httputil.NewSingleHostReverseProxy(remote)
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
r.Host = remote.Host
if token, ok := ctxpkg.ContextGetToken(r.Context()); ok {
r.Header.Set(ctxpkg.TokenHeader, token)
}
proxy.ServeHTTP(w, r)
})
r.Mount(rule.Endpoint, handler)
}
_ = chi.Walk(r, func(method string, route string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error {
log.Debug().Str("service", "reverseproxy").Str("method", method).Str("route", route).Int("middlewares", len(middlewares)).Msg("serving endpoint")
return nil
})
return &svc{router: r}, nil
}
func (s *svc) Close() error {
return nil
}
func (s *svc) Prefix() string {
// This service will be served at root
return ""
}
func (s *svc) Unprotected() []string {
// TODO: If the services which will be served via the reverse proxy have unprotected endpoints,
// we won't be able to support those at the moment.
return []string{}
}
func (s *svc) Handler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
s.router.ServeHTTP(w, r)
})
}
@@ -0,0 +1,143 @@
// Copyright 2018-2023 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 sciencemesh
import (
"context"
"encoding/json"
"errors"
"net/http"
"strings"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
rpcv1beta1 "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
ocmpb "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1"
"github.com/opencloud-eu/reva/v2/internal/http/services/reqres"
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/opencloud-eu/reva/v2/pkg/rhttp/router"
)
type appsHandler struct {
gatewaySelector *pool.Selector[gateway.GatewayAPIClient]
ocmMountPoint string
}
func (h *appsHandler) init(c *config) error {
var err error
h.gatewaySelector, err = pool.GatewaySelector(c.GatewaySvc)
if err != nil {
return err
}
h.ocmMountPoint = c.OCMMountPoint
return nil
}
func (h *appsHandler) shareInfo(p string) (*ocmpb.ShareId, string) {
p = strings.TrimPrefix(p, h.ocmMountPoint)
shareID, rel := router.ShiftPath(p)
if len(rel) > 0 {
rel = rel[1:]
}
return &ocmpb.ShareId{OpaqueId: shareID}, rel
}
func (h *appsHandler) OpenInApp(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
if err := r.ParseForm(); err != nil {
reqres.WriteError(w, r, reqres.APIErrorInvalidParameter, "parameters could not be parsed", nil)
return
}
path := r.Form.Get("file")
if path == "" {
reqres.WriteError(w, r, reqres.APIErrorInvalidParameter, "missing file", nil)
return
}
shareID, rel := h.shareInfo(path)
template, err := h.webappTemplate(ctx, shareID)
if err != nil {
var e errtypes.NotFound
if errors.As(err, &e) {
reqres.WriteError(w, r, reqres.APIErrorNotFound, e.Error(), nil)
}
reqres.WriteError(w, r, reqres.APIErrorServerError, err.Error(), err)
return
}
url := resolveTemplate(template, rel)
if err := json.NewEncoder(w).Encode(map[string]any{
"app_url": url,
}); err != nil {
reqres.WriteError(w, r, reqres.APIErrorServerError, "error marshalling JSON response", err)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
}
func (h *appsHandler) webappTemplate(ctx context.Context, id *ocmpb.ShareId) (string, error) {
gc, err := h.gatewaySelector.Next()
if err != nil {
return "", err
}
res, err := gc.GetReceivedOCMShare(ctx, &ocmpb.GetReceivedOCMShareRequest{
Ref: &ocmpb.ShareReference{
Spec: &ocmpb.ShareReference_Id{
Id: id,
},
},
})
if err != nil {
return "", err
}
if res.Status.Code != rpcv1beta1.Code_CODE_OK {
if res.Status.Code == rpcv1beta1.Code_CODE_NOT_FOUND {
return "", errtypes.NotFound(res.Status.Message)
}
return "", errtypes.InternalError(res.Status.Message)
}
webapp, ok := getWebappProtocol(res.Share.Protocols)
if !ok {
return "", errtypes.BadRequest("share does not contain webapp protocol")
}
return webapp.UriTemplate, nil
}
func getWebappProtocol(protocols []*ocmpb.Protocol) (*ocmpb.WebappProtocol, bool) {
for _, p := range protocols {
if t, ok := p.Term.(*ocmpb.Protocol_WebappOptions); ok {
return t.WebappOptions, true
}
}
return nil, false
}
func resolveTemplate(template string, rel string) string {
// the template is of type "https://open-cloud-mesh.org/s/share-hash/{relative-path-to-shared-resource}"
return strings.Replace(template, "{relative-path-to-shared-resource}", rel, 1)
}
@@ -0,0 +1,92 @@
// Copyright 2018-2023 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 sciencemesh
import (
"encoding/json"
"errors"
"net/http"
"strings"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
providerpb "github.com/cs3org/go-cs3apis/cs3/ocm/provider/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
"github.com/opencloud-eu/reva/v2/internal/http/services/reqres"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
)
type providersHandler struct {
gatewaySelector *pool.Selector[gateway.GatewayAPIClient]
}
func (h *providersHandler) init(c *config) error {
var err error
h.gatewaySelector, err = pool.GatewaySelector(c.GatewaySvc)
if err != nil {
return err
}
return nil
}
type provider struct {
FullName string `json:"full_name"`
Domain string `json:"domain"`
}
// ListProviders lists all the providers filtering by the `search` query parameter.
func (h *providersHandler) ListProviders(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
term := strings.ToLower(r.URL.Query().Get("search"))
gc, err := h.gatewaySelector.Next()
if err != nil {
reqres.WriteError(w, r, reqres.APIErrorServerError, "error selecting gateway client", err)
return
}
listRes, err := gc.ListAllProviders(ctx, &providerpb.ListAllProvidersRequest{})
if err != nil {
reqres.WriteError(w, r, reqres.APIErrorServerError, "error listing all providers", err)
return
}
if listRes.Status.Code != rpc.Code_CODE_OK {
reqres.WriteError(w, r, reqres.APIErrorServerError, listRes.Status.Message, errors.New(listRes.Status.Message))
return
}
filtered := []*provider{}
for _, p := range listRes.Providers {
if strings.Contains(strings.ToLower(p.FullName), term) ||
strings.Contains(strings.ToLower(p.Domain), term) {
filtered = append(filtered, &provider{
FullName: p.FullName,
Domain: p.Domain,
})
}
}
if err := json.NewEncoder(w).Encode(filtered); err != nil {
reqres.WriteError(w, r, reqres.APIErrorServerError, "error encoding response in json", err)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
}
@@ -0,0 +1,145 @@
// Copyright 2018-2023 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 sciencemesh
import (
"net/http"
"github.com/go-chi/chi/v5"
"github.com/rs/zerolog"
"github.com/opencloud-eu/reva/v2/pkg/appctx"
"github.com/opencloud-eu/reva/v2/pkg/rhttp/global"
"github.com/opencloud-eu/reva/v2/pkg/sharedconf"
"github.com/opencloud-eu/reva/v2/pkg/utils/cfg"
)
func init() {
global.Register("sciencemesh", New)
}
// New returns a new sciencemesh service.
func New(m map[string]interface{}, _ *zerolog.Logger) (global.Service, error) {
var c config
if err := cfg.Decode(m, &c); err != nil {
return nil, err
}
r := chi.NewRouter()
s := &svc{
conf: &c,
router: r,
}
if err := s.routerInit(); err != nil {
return nil, err
}
return s, nil
}
// Close performs cleanup.
func (s *svc) Close() error {
return nil
}
type config struct {
Prefix string `mapstructure:"prefix"`
GatewaySvc string `mapstructure:"gatewaysvc" validate:"required"`
ProviderDomain string `mapstructure:"provider_domain" validate:"required"`
MeshDirectoryURL string `mapstructure:"mesh_directory_url"`
OCMMountPoint string `mapstructure:"ocm_mount_point"`
Events EventOptions `mapstructure:"events"`
}
// EventOptions are the configurable options for events
type EventOptions struct {
Endpoint string `mapstructure:"natsaddress"`
Cluster string `mapstructure:"natsclusterid"`
TLSInsecure bool `mapstructure:"tlsinsecure"`
TLSRootCACertificate string `mapstructure:"tlsrootcacertificate"`
EnableTLS bool `mapstructure:"enabletls"`
AuthUsername string `mapstructure:"authusername"`
AuthPassword string `mapstructure:"authpassword"`
}
func (c *config) ApplyDefaults() {
if c.Prefix == "" {
c.Prefix = "sciencemesh"
}
if c.OCMMountPoint == "" {
c.OCMMountPoint = "/ocm"
}
c.GatewaySvc = sharedconf.GetGatewaySVC(c.GatewaySvc)
}
type svc struct {
conf *config
router chi.Router
}
func (s *svc) routerInit() error {
tokenHandler := new(tokenHandler)
if err := tokenHandler.init(s.conf); err != nil {
return err
}
providersHandler := new(providersHandler)
if err := providersHandler.init(s.conf); err != nil {
return err
}
sharesHandler := new(sharesHandler)
if err := sharesHandler.init(s.conf); err != nil {
return err
}
appsHandler := new(appsHandler)
if err := appsHandler.init(s.conf); err != nil {
return err
}
s.router.Post("/generate-invite", tokenHandler.Generate)
s.router.Get("/list-invite", tokenHandler.ListInvite)
s.router.Post("/accept-invite", tokenHandler.AcceptInvite)
s.router.Get("/find-accepted-users", tokenHandler.FindAccepted)
s.router.Delete("/delete-accepted-user", tokenHandler.DeleteAccepted)
s.router.Get("/list-providers", providersHandler.ListProviders)
s.router.Post("/create-share", sharesHandler.CreateShare)
s.router.Post("/open-in-app", appsHandler.OpenInApp)
return nil
}
func (s *svc) Prefix() string {
return s.conf.Prefix
}
func (s *svc) Unprotected() []string {
return nil
}
func (s *svc) Handler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log := appctx.GetLogger(r.Context())
log.Debug().Str("path", r.URL.Path).Msg("sciencemesh routing")
// unset raw path, otherwise chi uses it to route and then fails to match percent encoded path segments
r.URL.RawPath = ""
s.router.ServeHTTP(w, r)
})
}

Some files were not shown because too many files have changed in this diff Show More