From 034d89876dec188a021c1317780eb5b27568b684 Mon Sep 17 00:00:00 2001 From: Nicola Murino Date: Sun, 6 Dec 2020 08:19:41 +0100 Subject: [PATCH] webdav: fix proppatch handling also respect login delay for cached webdav users and check the home dir as soon as the user authenticates Fixes #239 --- dataprovider/dataprovider.go | 15 +++++++++++++- docs/dare.md | 2 +- docs/kms.md | 2 +- vfs/cryptfs.go | 13 ++++-------- webdavd/handler.go | 3 ++- webdavd/server.go | 38 ++++++++++++++++++++---------------- webdavd/webdavd_test.go | 38 ++++++++++++++++++++++++++++++++++++ 7 files changed, 81 insertions(+), 30 deletions(-) diff --git a/dataprovider/dataprovider.go b/dataprovider/dataprovider.go index 336d33a1..3b55aab5 100644 --- a/dataprovider/dataprovider.go +++ b/dataprovider/dataprovider.go @@ -582,7 +582,11 @@ func UpdateLastLogin(user User) error { lastLogin := utils.GetTimeFromMsecSinceEpoch(user.LastLogin) diff := -time.Until(lastLogin) if diff < 0 || diff > lastLoginMinDelay { - return provider.updateLastLogin(user.Username) + err := provider.updateLastLogin(user.Username) + if err == nil { + updateWebDavCachedUserLastLogin(user.Username) + } + return err } return nil } @@ -2132,6 +2136,15 @@ func updateVFoldersQuotaAfterRestore(foldersToScan []string) { } } +func updateWebDavCachedUserLastLogin(username string) { + result, ok := webDAVUsersCache.Load(username) + if ok { + cachedUser := result.(*CachedUser) + cachedUser.User.LastLogin = utils.GetTimeAsMsSinceEpoch(time.Now()) + webDAVUsersCache.Store(cachedUser.User.Username, cachedUser) + } +} + // CacheWebDAVUser add a user to the WebDAV cache func CacheWebDAVUser(cachedUser *CachedUser, maxSize int) { if maxSize > 0 { diff --git a/docs/dare.md b/docs/dare.md index 2b9e1cd1..993be59d 100644 --- a/docs/dare.md +++ b/docs/dare.md @@ -1,6 +1,6 @@ # Data At Rest Encryption (DARE) -SFTPGo supports data at-rest encryption via the `cryptfs` virtual file system, in this mode SFTPGo transparently encrypts and decrypts data (to/from the disk) on-the-fly during uploads and/or downloads, making sure that the files at-rest on the server-side are always encrypted. +SFTPGo supports data at-rest encryption via its `cryptfs` virtual file system, in this mode SFTPGo transparently encrypts and decrypts data (to/from the disk) on-the-fly during uploads and/or downloads, making sure that the files at-rest on the server-side are always encrypted. So, because of the way it works, as described here above, when you set up an encrypted filesystem for a user you need to make sure it points to an empty path/directory (that has no files in it). Otherwise, it would try to decrypt existing files that are not encrypted in the first place and fail. diff --git a/docs/kms.md b/docs/kms.md index 85d985c1..12e63c23 100644 --- a/docs/kms.md +++ b/docs/kms.md @@ -1,6 +1,6 @@ # Key Management Services -SFTPGo stores sensitive data such as Cloud accounts credentials. This data are stored as ciphertext and only loaded to RAM in plaintext when needed. +SFTPGo stores sensitive data such as Cloud accounts credentials or passphrases to derive per-object encryption keys. These data are stored as ciphertext and only loaded to RAM in plaintext when needed. ## Supported Services for encryption and decryption diff --git a/vfs/cryptfs.go b/vfs/cryptfs.go index d025413d..3ec7d770 100644 --- a/vfs/cryptfs.go +++ b/vfs/cryptfs.go @@ -328,19 +328,14 @@ func (h *encryptedFileHeader) Store(f *os.File) error { } func (h *encryptedFileHeader) Load(f *os.File) error { - vers := make([]byte, 1) - _, err := io.ReadFull(f, vers) + header := make([]byte, 1+nonceV10Size) + _, err := io.ReadFull(f, header) if err != nil { return err } - h.version = vers[0] + h.version = header[0] if h.version == version10 { - nonce := make([]byte, nonceV10Size) - _, err := io.ReadFull(f, nonce) - if err != nil { - return err - } - h.nonce = nonce + h.nonce = header[1:] return nil } return fmt.Errorf("unsupported encryption version: %v", h.version) diff --git a/webdavd/handler.go b/webdavd/handler.go index 03069e3f..463b3852 100644 --- a/webdavd/handler.go +++ b/webdavd/handler.go @@ -143,7 +143,8 @@ func (c *Connection) OpenFile(ctx context.Context, name string, flag int, perm o if err != nil { return nil, c.GetFsError(err) } - if flag == os.O_RDONLY { + + if flag == os.O_RDONLY || c.request.Method == "PROPPATCH" { // Download, Stat, Readdir or simply open/close return c.getFile(p, name) } diff --git a/webdavd/server.go b/webdavd/server.go index a7ee27aa..368913cf 100644 --- a/webdavd/server.go +++ b/webdavd/server.go @@ -84,6 +84,20 @@ func (s *webDavServer) listenAndServe() error { return httpServer.ListenAndServe() } +func (s *webDavServer) checkRequestMethod(ctx context.Context, r *http.Request, connection *Connection, prefix string) { + // see RFC4918, section 9.4 + if r.Method == http.MethodGet { + p := strings.TrimPrefix(path.Clean(r.URL.Path), prefix) + info, err := connection.Stat(ctx, p) + if err == nil && info.IsDir() { + r.Method = "PROPFIND" + if r.Header.Get("Depth") == "" { + r.Header.Add("Depth", "1") + } + } + } +} + // ServeHTTP implements the http.Handler interface func (s *webDavServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { defer func() { @@ -97,7 +111,7 @@ func (s *webDavServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { http.Error(w, common.ErrConnectionDenied.Error(), http.StatusForbidden) return } - user, isCached, lockSystem, err := s.authenticate(r) + user, _, lockSystem, err := s.authenticate(r) if err != nil { w.Header().Set("WWW-Authenticate", "Basic realm=\"SFTPGo WebDAV\"") http.Error(w, err401.Error(), http.StatusUnauthorized) @@ -135,24 +149,10 @@ func (s *webDavServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { common.Connections.Add(connection) defer common.Connections.Remove(connection.GetID()) - if !isCached { - // we check the home directory only if the user is not cached - connection.Fs.CheckRootPath(connection.GetUsername(), user.GetUID(), user.GetGID()) - } dataprovider.UpdateLastLogin(user) //nolint:errcheck prefix := path.Join("/", user.Username) - // see RFC4918, section 9.4 - if r.Method == http.MethodGet { - p := strings.TrimPrefix(path.Clean(r.URL.Path), prefix) - info, err := connection.Stat(ctx, p) - if err == nil && info.IsDir() { - r.Method = "PROPFIND" - if r.Header.Get("Depth") == "" { - r.Header.Add("Depth", "1") - } - } - } + s.checkRequestMethod(ctx, r, connection, prefix) handler := webdav.Handler{ Prefix: prefix, @@ -199,8 +199,12 @@ func (s *webDavServer) authenticate(r *http.Request) (dataprovider.User, bool, w cachedUser.Expiration = time.Now().Add(time.Duration(s.config.Cache.Users.ExpirationTime) * time.Minute) } dataprovider.CacheWebDAVUser(cachedUser, s.config.Cache.Users.MaxSize) + tempFs, err := user.GetFilesystem("temp") + if err == nil { + tempFs.CheckRootPath(user.Username, user.UID, user.GID) + } } - return user, false, lockSystem, err + return user, false, lockSystem, nil } func (s *webDavServer) validateUser(user dataprovider.User, r *http.Request) (string, error) { diff --git a/webdavd/webdavd_test.go b/webdavd/webdavd_test.go index 2417b602..92bc1c8c 100644 --- a/webdavd/webdavd_test.go +++ b/webdavd/webdavd_test.go @@ -1,6 +1,7 @@ package webdavd_test import ( + "bytes" "crypto/rand" "encoding/json" "errors" @@ -360,6 +361,43 @@ func TestBasicHandlingCryptFs(t *testing.T) { assert.Len(t, common.Connections.GetStats(), 0) } +func TestPropPatch(t *testing.T) { + for _, u := range []dataprovider.User{getTestUser(), getTestUserWithCryptFs()} { + user, _, err := httpd.AddUser(u, http.StatusOK) + assert.NoError(t, err) + client := getWebDavClient(user) + assert.NoError(t, checkBasicFunc(client)) + + testFilePath := filepath.Join(homeBasePath, testFileName) + testFileSize := int64(65535) + err = createTestFile(testFilePath, testFileSize) + assert.NoError(t, err) + err = uploadFile(testFilePath, testFileName, testFileSize, client) + assert.NoError(t, err) + httpClient := httpclient.GetHTTPClient() + propatchBody := `Wed, 04 Nov 2020 13:25:51 GMTSat, 05 Dec 2020 21:16:12 GMTWed, 04 Nov 2020 13:25:51 GMT00000000` + req, err := http.NewRequest("PROPPATCH", fmt.Sprintf("http://%v/%v/%v", webDavServerAddr, user.Username, testFileName), bytes.NewReader([]byte(propatchBody))) + assert.NoError(t, err) + req.SetBasicAuth(u.Username, u.Password) + resp, err := httpClient.Do(req) + assert.NoError(t, err) + assert.Equal(t, 207, resp.StatusCode) + err = resp.Body.Close() + assert.NoError(t, err) + info, err := client.Stat(testFileName) + if assert.NoError(t, err) { + assert.Equal(t, testFileSize, info.Size()) + } + err = os.Remove(testFilePath) + assert.NoError(t, err) + _, err = httpd.RemoveUser(user, http.StatusOK) + assert.NoError(t, err) + err = os.RemoveAll(user.GetHomeDir()) + assert.NoError(t, err) + assert.Len(t, common.Connections.GetStats(), 0) + } +} + func TestLoginInvalidPwd(t *testing.T) { u := getTestUser() user, _, err := httpd.AddUser(u, http.StatusOK)