Browse Source

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
Nicola Murino 4 years ago
parent
commit
034d89876d
7 changed files with 81 additions and 30 deletions
  1. 14 1
      dataprovider/dataprovider.go
  2. 1 1
      docs/dare.md
  3. 1 1
      docs/kms.md
  4. 4 9
      vfs/cryptfs.go
  5. 2 1
      webdavd/handler.go
  6. 21 17
      webdavd/server.go
  7. 38 0
      webdavd/webdavd_test.go

+ 14 - 1
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 {

+ 1 - 1
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.
 

+ 1 - 1
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
 

+ 4 - 9
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)

+ 2 - 1
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)
 	}

+ 21 - 17
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) {

+ 38 - 0
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 := `<?xml version="1.0" encoding="utf-8" ?><D:propertyupdate xmlns:D="DAV:" xmlns:Z="urn:schemas-microsoft-com:"><D:set><D:prop><Z:Win32CreationTime>Wed, 04 Nov 2020 13:25:51 GMT</Z:Win32CreationTime><Z:Win32LastAccessTime>Sat, 05 Dec 2020 21:16:12 GMT</Z:Win32LastAccessTime><Z:Win32LastModifiedTime>Wed, 04 Nov 2020 13:25:51 GMT</Z:Win32LastModifiedTime><Z:Win32FileAttributes>00000000</Z:Win32FileAttributes></D:prop></D:set></D:propertyupdate>`
+		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)