github.com/cs3org/reva/v2@v2.27.7/internal/http/services/owncloud/ocdav/dav.go (about) 1 // Copyright 2018-2021 CERN 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 // 15 // In applying this license, CERN does not waive the privileges and immunities 16 // granted to it by virtue of its status as an Intergovernmental Organization 17 // or submit itself to any jurisdiction. 18 19 package ocdav 20 21 import ( 22 "context" 23 "fmt" 24 "net/http" 25 "path" 26 "path/filepath" 27 "strings" 28 29 gatewayv1beta1 "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" 30 userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" 31 rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" 32 link "github.com/cs3org/go-cs3apis/cs3/sharing/link/v1beta1" 33 provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" 34 "github.com/cs3org/reva/v2/internal/http/services/owncloud/ocdav/config" 35 "github.com/cs3org/reva/v2/internal/http/services/owncloud/ocdav/errors" 36 "github.com/cs3org/reva/v2/internal/http/services/owncloud/ocdav/net" 37 "github.com/cs3org/reva/v2/pkg/appctx" 38 ctxpkg "github.com/cs3org/reva/v2/pkg/ctx" 39 "github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool" 40 "github.com/cs3org/reva/v2/pkg/rhttp/router" 41 "github.com/cs3org/reva/v2/pkg/storage/utils/grants" 42 "github.com/cs3org/reva/v2/pkg/storagespace" 43 "github.com/cs3org/reva/v2/pkg/utils" 44 "go.opentelemetry.io/otel/trace" 45 "google.golang.org/grpc/metadata" 46 ) 47 48 const ( 49 _trashbinPath = "trash-bin" 50 51 // WwwAuthenticate captures the Www-Authenticate header string. 52 WwwAuthenticate = "Www-Authenticate" 53 ) 54 55 const ( 56 ErrListingMembers = "ERR_LISTING_MEMBERS_NOT_ALLOWED" 57 ErrInvalidCredentials = "ERR_INVALID_CREDENTIALS" 58 ErrMissingBasicAuth = "ERR_MISSING_BASIC_AUTH" 59 ErrMissingBearerAuth = "ERR_MISSING_BEARER_AUTH" 60 ErrFileNotFoundInRoot = "ERR_FILE_NOT_FOUND_IN_ROOT" 61 ) 62 63 // DavHandler routes to the different sub handlers 64 type DavHandler struct { 65 AvatarsHandler *AvatarsHandler 66 FilesHandler *WebDavHandler 67 FilesHomeHandler *WebDavHandler 68 MetaHandler *MetaHandler 69 TrashbinHandler *TrashbinHandler 70 SpacesHandler *SpacesHandler 71 PublicFolderHandler *WebDavHandler 72 PublicFileHandler *PublicFileHandler 73 SharesHandler *WebDavHandler 74 OCMSharesHandler *WebDavHandler 75 } 76 77 func (h *DavHandler) init(c *config.Config) error { 78 h.AvatarsHandler = new(AvatarsHandler) 79 if err := h.AvatarsHandler.init(c); err != nil { 80 return err 81 } 82 h.FilesHandler = new(WebDavHandler) 83 if err := h.FilesHandler.init(c.FilesNamespace, false); err != nil { 84 return err 85 } 86 h.FilesHomeHandler = new(WebDavHandler) 87 if err := h.FilesHomeHandler.init(c.WebdavNamespace, true); err != nil { 88 return err 89 } 90 h.MetaHandler = new(MetaHandler) 91 if err := h.MetaHandler.init(c); err != nil { 92 return err 93 } 94 h.TrashbinHandler = new(TrashbinHandler) 95 if err := h.TrashbinHandler.init(c); err != nil { 96 return err 97 } 98 99 h.SpacesHandler = new(SpacesHandler) 100 if err := h.SpacesHandler.init(c); err != nil { 101 return err 102 } 103 104 h.PublicFolderHandler = new(WebDavHandler) 105 if err := h.PublicFolderHandler.init("public", true); err != nil { // jail public file requests to /public/ prefix 106 return err 107 } 108 109 h.PublicFileHandler = new(PublicFileHandler) 110 if err := h.PublicFileHandler.init("public"); err != nil { // jail public file requests to /public/ prefix 111 return err 112 } 113 114 h.OCMSharesHandler = new(WebDavHandler) 115 if err := h.OCMSharesHandler.init(c.OCMNamespace, true); err != nil { 116 return err 117 } 118 119 return nil 120 } 121 122 func isOwner(userIDorName string, user *userv1beta1.User) bool { 123 return userIDorName != "" && (userIDorName == user.Id.OpaqueId || strings.EqualFold(userIDorName, user.Username)) 124 } 125 126 // Handler handles requests 127 func (h *DavHandler) Handler(s *svc) http.Handler { 128 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 129 ctx := r.Context() 130 log := appctx.GetLogger(ctx) 131 132 // if there is no file in the request url we assume the request url is: "/remote.php/dav/files" 133 // https://github.com/owncloud/core/blob/18475dac812064b21dabcc50f25ef3ffe55691a5/tests/acceptance/features/apiWebdavOperations/propfind.feature 134 if r.URL.Path == "/files" { 135 log.Debug().Str("path", r.URL.Path).Msg("method not allowed") 136 contextUser, ok := ctxpkg.ContextGetUser(ctx) 137 if ok { 138 r.URL.Path = path.Join(r.URL.Path, contextUser.Username) 139 } 140 141 if r.Header.Get(net.HeaderDepth) == "" { 142 w.WriteHeader(http.StatusMethodNotAllowed) 143 b, err := errors.Marshal(http.StatusMethodNotAllowed, "Listing members of this collection is disabled", "", ErrListingMembers) 144 if err != nil { 145 log.Error().Msgf("error marshaling xml response: %s", b) 146 w.WriteHeader(http.StatusInternalServerError) 147 return 148 } 149 _, err = w.Write(b) 150 if err != nil { 151 log.Error().Msgf("error writing xml response: %s", b) 152 w.WriteHeader(http.StatusInternalServerError) 153 return 154 } 155 return 156 } 157 } 158 159 var head string 160 head, r.URL.Path = router.ShiftPath(r.URL.Path) 161 162 switch head { 163 case "avatars": 164 h.AvatarsHandler.Handler(s).ServeHTTP(w, r) 165 case "files": 166 var requestUserID string 167 var oldPath = r.URL.Path 168 169 // detect and check current user in URL 170 requestUserID, r.URL.Path = router.ShiftPath(r.URL.Path) 171 172 // note: some requests like OPTIONS don't forward the user 173 contextUser, ok := ctxpkg.ContextGetUser(ctx) 174 if ok && isOwner(requestUserID, contextUser) { 175 // use home storage handler when user was detected 176 base := path.Join(ctx.Value(net.CtxKeyBaseURI).(string), "files", requestUserID) 177 ctx := context.WithValue(ctx, net.CtxKeyBaseURI, base) 178 r = r.WithContext(ctx) 179 180 h.FilesHomeHandler.Handler(s).ServeHTTP(w, r) 181 } else { 182 r.URL.Path = oldPath 183 base := path.Join(ctx.Value(net.CtxKeyBaseURI).(string), "files") 184 ctx := context.WithValue(ctx, net.CtxKeyBaseURI, base) 185 r = r.WithContext(ctx) 186 187 h.FilesHandler.Handler(s).ServeHTTP(w, r) 188 } 189 case "meta": 190 base := path.Join(ctx.Value(net.CtxKeyBaseURI).(string), "meta") 191 ctx = context.WithValue(ctx, net.CtxKeyBaseURI, base) 192 r = r.WithContext(ctx) 193 h.MetaHandler.Handler(s).ServeHTTP(w, r) 194 case "ocm": 195 base := path.Join(ctx.Value(net.CtxKeyBaseURI).(string), "ocm") 196 ctx := context.WithValue(ctx, net.CtxKeyBaseURI, base) 197 c, err := s.gatewaySelector.Next() 198 if err != nil { 199 w.WriteHeader(http.StatusNotFound) 200 return 201 } 202 203 // OC10 and Nextcloud (OCM 1.0) are using basic auth for carrying the 204 // ocm share id. 205 var ocmshare, sharedSecret string 206 username, _, ok := r.BasicAuth() 207 if ok { 208 // OCM 1.0 209 ocmshare = username 210 sharedSecret = username 211 r.URL.Path = filepath.Join("/", ocmshare, r.URL.Path) 212 } else { 213 ocmshare, _ = router.ShiftPath(r.URL.Path) 214 sharedSecret = strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ") 215 } 216 217 authRes, err := handleOCMAuth(ctx, c, ocmshare, sharedSecret) 218 switch { 219 case err != nil: 220 log.Error().Err(err).Msg("error during ocm authentication") 221 w.WriteHeader(http.StatusInternalServerError) 222 return 223 case authRes.Status.Code == rpc.Code_CODE_PERMISSION_DENIED: 224 log.Debug().Str("ocmshare", ocmshare).Msg("permission denied") 225 fallthrough 226 case authRes.Status.Code == rpc.Code_CODE_UNAUTHENTICATED: 227 log.Debug().Str("ocmshare", ocmshare).Msg("unauthorized") 228 w.WriteHeader(http.StatusUnauthorized) 229 return 230 case authRes.Status.Code == rpc.Code_CODE_NOT_FOUND: 231 log.Debug().Str("ocmshare", ocmshare).Msg("not found") 232 w.WriteHeader(http.StatusNotFound) 233 return 234 case authRes.Status.Code != rpc.Code_CODE_OK: 235 log.Error().Str("ocmshare", ocmshare).Interface("status", authRes.Status).Msg("grpc auth request failed") 236 w.WriteHeader(http.StatusInternalServerError) 237 return 238 } 239 240 ctx = ctxpkg.ContextSetToken(ctx, authRes.Token) 241 ctx = ctxpkg.ContextSetUser(ctx, authRes.User) 242 ctx = metadata.AppendToOutgoingContext(ctx, ctxpkg.TokenHeader, authRes.Token) 243 244 log.Debug().Str("ocmshare", ocmshare).Interface("user", authRes.User).Msg("OCM user authenticated") 245 246 r = r.WithContext(ctx) 247 h.OCMSharesHandler.Handler(s).ServeHTTP(w, r) 248 case "trash-bin": 249 base := path.Join(ctx.Value(net.CtxKeyBaseURI).(string), "trash-bin") 250 ctx := context.WithValue(ctx, net.CtxKeyBaseURI, base) 251 r = r.WithContext(ctx) 252 h.TrashbinHandler.Handler(s).ServeHTTP(w, r) 253 case "spaces": 254 base := path.Join(ctx.Value(net.CtxKeyBaseURI).(string), "spaces") 255 ctx := context.WithValue(ctx, net.CtxKeyBaseURI, base) 256 r = r.WithContext(ctx) 257 h.SpacesHandler.Handler(s, h.TrashbinHandler).ServeHTTP(w, r) 258 case "public-files": 259 base := path.Join(ctx.Value(net.CtxKeyBaseURI).(string), "public-files") 260 ctx = context.WithValue(ctx, net.CtxKeyBaseURI, base) 261 262 var res *gatewayv1beta1.AuthenticateResponse 263 token, _ := router.ShiftPath(r.URL.Path) 264 var hasValidBasicAuthHeader bool 265 var pass string 266 var err error 267 // If user is authenticated 268 _, userExists := ctxpkg.ContextGetUser(ctx) 269 if userExists { 270 client, err := s.gatewaySelector.Next() 271 if err != nil { 272 log.Error().Err(err).Msg("error sending grpc stat request") 273 w.WriteHeader(http.StatusInternalServerError) 274 return 275 } 276 psRes, err := client.GetPublicShare(ctx, &link.GetPublicShareRequest{ 277 Ref: &link.PublicShareReference{ 278 Spec: &link.PublicShareReference_Token{ 279 Token: token, 280 }, 281 }}) 282 if err != nil && !strings.Contains(err.Error(), "core access token not found") { 283 log.Error().Err(err).Msg("error sending grpc stat request") 284 w.WriteHeader(http.StatusInternalServerError) 285 return 286 } 287 // If the link is internal then 307 redirect 288 if psRes.Status.Code == rpc.Code_CODE_OK && grants.PermissionsEqual(psRes.Share.Permissions.GetPermissions(), &provider.ResourcePermissions{}) { 289 if psRes.GetShare().GetResourceId() != nil { 290 rUrl := path.Join("/dav/spaces", storagespace.FormatResourceID(psRes.GetShare().GetResourceId())) 291 http.Redirect(w, r, rUrl, http.StatusTemporaryRedirect) 292 return 293 } 294 log.Debug().Str("token", token).Interface("status", psRes.Status).Msg("resource id not found") 295 w.WriteHeader(http.StatusNotFound) 296 return 297 } 298 } 299 300 if _, pass, hasValidBasicAuthHeader = r.BasicAuth(); hasValidBasicAuthHeader { 301 res, err = handleBasicAuth(r.Context(), s.gatewaySelector, token, pass) 302 } else { 303 q := r.URL.Query() 304 sig := q.Get("signature") 305 expiration := q.Get("expiration") 306 // We restrict the pre-signed urls to downloads. 307 if sig != "" && expiration != "" && !(r.Method == http.MethodGet || r.Method == http.MethodHead) { 308 w.WriteHeader(http.StatusUnauthorized) 309 return 310 } 311 res, err = handleSignatureAuth(r.Context(), s.gatewaySelector, token, sig, expiration) 312 } 313 314 switch { 315 case err != nil: 316 w.WriteHeader(http.StatusInternalServerError) 317 return 318 case res.Status.Code == rpc.Code_CODE_PERMISSION_DENIED: 319 fallthrough 320 case res.Status.Code == rpc.Code_CODE_UNAUTHENTICATED: 321 w.WriteHeader(http.StatusUnauthorized) 322 if hasValidBasicAuthHeader { 323 b, err := errors.Marshal(http.StatusUnauthorized, "Username or password was incorrect", "", ErrInvalidCredentials) 324 errors.HandleWebdavError(log, w, b, err) 325 return 326 } 327 b, err := errors.Marshal(http.StatusUnauthorized, "No 'Authorization: Basic' header found", "", ErrMissingBasicAuth) 328 errors.HandleWebdavError(log, w, b, err) 329 return 330 case res.Status.Code == rpc.Code_CODE_NOT_FOUND: 331 w.WriteHeader(http.StatusNotFound) 332 return 333 case res.Status.Code != rpc.Code_CODE_OK: 334 w.WriteHeader(http.StatusInternalServerError) 335 return 336 } 337 338 if userExists { 339 // Build new context without an authenticated user. 340 // the public link should be resolved by the 'publicshares' authenticated user 341 baseURI := ctx.Value(net.CtxKeyBaseURI).(string) 342 logger := appctx.GetLogger(ctx) 343 span := trace.SpanFromContext(ctx) 344 span.End() 345 ctx = trace.ContextWithSpan(context.Background(), span) 346 ctx = appctx.WithLogger(ctx, logger) 347 ctx = context.WithValue(ctx, net.CtxKeyBaseURI, baseURI) 348 } 349 ctx = ctxpkg.ContextSetToken(ctx, res.Token) 350 ctx = ctxpkg.ContextSetUser(ctx, res.User) 351 ctx = metadata.AppendToOutgoingContext(ctx, ctxpkg.TokenHeader, res.Token) 352 353 r = r.WithContext(ctx) 354 355 // the public share manager knew the token, but does the referenced target still exist? 356 sRes, err := getTokenStatInfo(ctx, s.gatewaySelector, token) 357 switch { 358 case err != nil: 359 log.Error().Err(err).Msg("error sending grpc stat request") 360 w.WriteHeader(http.StatusInternalServerError) 361 return 362 case sRes.Status.Code == rpc.Code_CODE_PERMISSION_DENIED: 363 fallthrough 364 case sRes.Status.Code == rpc.Code_CODE_OK && grants.PermissionsEqual(sRes.GetInfo().GetPermissionSet(), &provider.ResourcePermissions{}): 365 // If the link is internal 366 if !userExists { 367 w.Header().Add(WwwAuthenticate, fmt.Sprintf("Bearer realm=\"%s\", charset=\"UTF-8\"", r.Host)) 368 w.WriteHeader(http.StatusUnauthorized) 369 b, err := errors.Marshal(http.StatusUnauthorized, "No 'Authorization: Bearer' header found", "", ErrMissingBearerAuth) 370 errors.HandleWebdavError(log, w, b, err) 371 return 372 } 373 fallthrough 374 case sRes.Status.Code == rpc.Code_CODE_NOT_FOUND: 375 log.Debug().Str("token", token).Interface("status", res.Status).Msg("resource not found") 376 w.WriteHeader(http.StatusNotFound) // log the difference 377 return 378 case sRes.Status.Code == rpc.Code_CODE_UNAUTHENTICATED: 379 log.Debug().Str("token", token).Interface("status", res.Status).Msg("unauthorized") 380 w.WriteHeader(http.StatusUnauthorized) 381 return 382 case sRes.Status.Code != rpc.Code_CODE_OK: 383 log.Error().Str("token", token).Interface("status", res.Status).Msg("grpc stat request failed") 384 w.WriteHeader(http.StatusInternalServerError) 385 return 386 } 387 log.Debug().Interface("statInfo", sRes.Info).Msg("Stat info from public link token path") 388 389 ctx := ContextWithTokenStatInfo(ctx, sRes.Info) 390 r = r.WithContext(ctx) 391 if sRes.Info.Type != provider.ResourceType_RESOURCE_TYPE_CONTAINER { 392 h.PublicFileHandler.Handler(s).ServeHTTP(w, r) 393 } else { 394 h.PublicFolderHandler.Handler(s).ServeHTTP(w, r) 395 } 396 397 default: 398 w.WriteHeader(http.StatusNotFound) 399 b, err := errors.Marshal(http.StatusNotFound, "File not found in root", "", ErrFileNotFoundInRoot) 400 errors.HandleWebdavError(log, w, b, err) 401 } 402 }) 403 } 404 405 func getTokenStatInfo(ctx context.Context, selector pool.Selectable[gatewayv1beta1.GatewayAPIClient], token string) (*provider.StatResponse, error) { 406 client, err := selector.Next() 407 if err != nil { 408 return nil, err 409 } 410 411 return client.Stat(ctx, &provider.StatRequest{Ref: &provider.Reference{ 412 ResourceId: &provider.ResourceId{ 413 StorageId: utils.PublicStorageProviderID, 414 SpaceId: utils.PublicStorageSpaceID, 415 OpaqueId: token, 416 }, 417 }}) 418 } 419 420 func handleBasicAuth(ctx context.Context, selector pool.Selectable[gatewayv1beta1.GatewayAPIClient], token, pw string) (*gatewayv1beta1.AuthenticateResponse, error) { 421 c, err := selector.Next() 422 if err != nil { 423 return nil, err 424 } 425 authenticateRequest := gatewayv1beta1.AuthenticateRequest{ 426 Type: "publicshares", 427 ClientId: token, 428 ClientSecret: "password|" + pw, 429 } 430 431 return c.Authenticate(ctx, &authenticateRequest) 432 } 433 434 func handleSignatureAuth(ctx context.Context, selector pool.Selectable[gatewayv1beta1.GatewayAPIClient], token, sig, expiration string) (*gatewayv1beta1.AuthenticateResponse, error) { 435 c, err := selector.Next() 436 if err != nil { 437 return nil, err 438 } 439 authenticateRequest := gatewayv1beta1.AuthenticateRequest{ 440 Type: "publicshares", 441 ClientId: token, 442 ClientSecret: "signature|" + sig + "|" + expiration, 443 } 444 445 return c.Authenticate(ctx, &authenticateRequest) 446 } 447 448 func handleOCMAuth(ctx context.Context, c gatewayv1beta1.GatewayAPIClient, ocmshare, sharedSecret string) (*gatewayv1beta1.AuthenticateResponse, error) { 449 return c.Authenticate(ctx, &gatewayv1beta1.AuthenticateRequest{ 450 Type: "ocmshares", 451 ClientId: ocmshare, 452 ClientSecret: sharedSecret, 453 }) 454 }