sftpgo/internal/sftpd/cryptfs_test.go
Nicola Murino 784b7585c1
remove end year from Copyright notice in files
so we don't have to update all the files every year

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-01-01 11:31:45 +01:00

509 lines
18 KiB
Go

// Copyright (C) 2019 Nicola Murino
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published
// by the Free Software Foundation, version 3.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package sftpd_test
import (
"crypto/sha256"
"fmt"
"net/http"
"os"
"path"
"path/filepath"
"testing"
"time"
"github.com/minio/sio"
"github.com/sftpgo/sdk"
"github.com/stretchr/testify/assert"
"github.com/drakkan/sftpgo/v2/internal/dataprovider"
"github.com/drakkan/sftpgo/v2/internal/httpdtest"
"github.com/drakkan/sftpgo/v2/internal/kms"
"github.com/drakkan/sftpgo/v2/internal/vfs"
)
const (
testPassphrase = "test passphrase"
)
func TestBasicSFTPCryptoHandling(t *testing.T) {
usePubKey := false
u := getTestUserWithCryptFs(usePubKey)
u.QuotaSize = 6553600
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
conn, client, err := getSftpClient(user, usePubKey)
if assert.NoError(t, err) {
defer conn.Close()
defer client.Close()
testFilePath := filepath.Join(homeBasePath, testFileName)
testFileSize := int64(65535)
encryptedFileSize, err := getEncryptedFileSize(testFileSize)
assert.NoError(t, err)
expectedQuotaSize := user.UsedQuotaSize + encryptedFileSize
expectedQuotaFiles := user.UsedQuotaFiles + 1
err = createTestFile(testFilePath, testFileSize)
assert.NoError(t, err)
err = sftpUploadFile(testFilePath, path.Join("/missing_dir", testFileName), testFileSize, client)
assert.Error(t, err)
err = sftpUploadFile(testFilePath, testFileName, testFileSize, client)
assert.NoError(t, err)
localDownloadPath := filepath.Join(homeBasePath, testDLFileName)
err = sftpDownloadFile(testFileName, localDownloadPath, testFileSize, client)
assert.NoError(t, err)
initialHash, err := computeHashForFile(sha256.New(), testFilePath)
assert.NoError(t, err)
downloadedFileHash, err := computeHashForFile(sha256.New(), localDownloadPath)
assert.NoError(t, err)
assert.Equal(t, initialHash, downloadedFileHash)
info, err := os.Stat(filepath.Join(user.HomeDir, testFileName))
if assert.NoError(t, err) {
assert.Equal(t, encryptedFileSize, info.Size())
}
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
assert.NoError(t, err)
assert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles)
assert.Equal(t, expectedQuotaSize, user.UsedQuotaSize)
result, err := client.ReadDir(".")
assert.NoError(t, err)
if assert.Len(t, result, 1) {
assert.Equal(t, testFileSize, result[0].Size())
}
info, err = client.Stat(testFileName)
if assert.NoError(t, err) {
assert.Equal(t, testFileSize, info.Size())
}
err = client.Remove(testFileName)
assert.NoError(t, err)
_, err = client.Lstat(testFileName)
assert.Error(t, err)
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
assert.NoError(t, err)
assert.Equal(t, expectedQuotaFiles-1, user.UsedQuotaFiles)
assert.Equal(t, expectedQuotaSize-encryptedFileSize, user.UsedQuotaSize)
err = os.Remove(testFilePath)
assert.NoError(t, err)
err = os.Remove(localDownloadPath)
assert.NoError(t, err)
}
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
}
func TestOpenReadWriteCryptoFs(t *testing.T) {
// read and write is not supported on crypto fs
usePubKey := false
u := getTestUserWithCryptFs(usePubKey)
u.QuotaSize = 6553600
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
conn, client, err := getSftpClient(user, usePubKey)
if assert.NoError(t, err) {
defer conn.Close()
defer client.Close()
sftpFile, err := client.OpenFile(testFileName, os.O_RDWR|os.O_CREATE|os.O_TRUNC)
if assert.NoError(t, err) {
testData := []byte("sample test data")
n, err := sftpFile.Write(testData)
assert.NoError(t, err)
assert.Equal(t, len(testData), n)
buffer := make([]byte, 128)
_, err = sftpFile.ReadAt(buffer, 1)
if assert.Error(t, err) {
assert.Contains(t, err.Error(), "SSH_FX_OP_UNSUPPORTED")
}
err = sftpFile.Close()
assert.NoError(t, err)
}
}
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
}
func TestEmptyFile(t *testing.T) {
usePubKey := true
u := getTestUserWithCryptFs(usePubKey)
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
conn, client, err := getSftpClient(user, usePubKey)
if assert.NoError(t, err) {
defer conn.Close()
defer client.Close()
sftpFile, err := client.OpenFile(testFileName, os.O_RDWR|os.O_CREATE|os.O_TRUNC)
if assert.NoError(t, err) {
testData := []byte("")
n, err := sftpFile.Write(testData)
assert.NoError(t, err)
assert.Equal(t, len(testData), n)
err = sftpFile.Close()
assert.NoError(t, err)
}
info, err := client.Stat(testFileName)
if assert.NoError(t, err) {
assert.Equal(t, int64(0), info.Size())
}
localDownloadPath := filepath.Join(homeBasePath, testDLFileName)
err = sftpDownloadFile(testFileName, localDownloadPath, 0, client)
assert.NoError(t, err)
encryptedFileSize, err := getEncryptedFileSize(0)
assert.NoError(t, err)
info, err = os.Stat(filepath.Join(user.HomeDir, testFileName))
if assert.NoError(t, err) {
assert.Equal(t, encryptedFileSize, info.Size())
}
err = os.Remove(localDownloadPath)
assert.NoError(t, err)
}
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
}
func TestUploadResumeCryptFs(t *testing.T) {
// resuming uploads is not supported
usePubKey := true
u := getTestUserWithCryptFs(usePubKey)
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
conn, client, err := getSftpClient(user, usePubKey)
if assert.NoError(t, err) {
defer conn.Close()
defer client.Close()
testFilePath := filepath.Join(homeBasePath, testFileName)
testFileSize := int64(65535)
appendDataSize := int64(65535)
err = createTestFile(testFilePath, testFileSize)
assert.NoError(t, err)
err = sftpUploadFile(testFilePath, testFileName, testFileSize, client)
assert.NoError(t, err)
err = appendToTestFile(testFilePath, appendDataSize)
assert.NoError(t, err)
err = sftpUploadResumeFile(testFilePath, testFileName, testFileSize, false, client)
if assert.Error(t, err) {
assert.Contains(t, err.Error(), "SSH_FX_OP_UNSUPPORTED")
}
}
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
}
func TestQuotaFileReplaceCryptFs(t *testing.T) {
usePubKey := false
u := getTestUserWithCryptFs(usePubKey)
u.QuotaFiles = 1000
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
testFileSize := int64(65535)
testFilePath := filepath.Join(homeBasePath, testFileName)
encryptedFileSize, err := getEncryptedFileSize(testFileSize)
assert.NoError(t, err)
conn, client, err := getSftpClient(user, usePubKey)
if assert.NoError(t, err) { //nolint:dupl
defer conn.Close()
defer client.Close()
expectedQuotaSize := user.UsedQuotaSize + encryptedFileSize
expectedQuotaFiles := user.UsedQuotaFiles + 1
err = createTestFile(testFilePath, testFileSize)
assert.NoError(t, err)
err = sftpUploadFile(testFilePath, testFileName, testFileSize, client)
assert.NoError(t, err)
// now replace the same file, the quota must not change
err = sftpUploadFile(testFilePath, testFileName, testFileSize, client)
assert.NoError(t, err)
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
assert.NoError(t, err)
assert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles)
assert.Equal(t, expectedQuotaSize, user.UsedQuotaSize)
// now create a symlink, replace it with a file and check the quota
// replacing a symlink is like uploading a new file
err = client.Symlink(testFileName, testFileName+".link") //nolint:goconst
assert.NoError(t, err)
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
assert.NoError(t, err)
assert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles)
assert.Equal(t, expectedQuotaSize, user.UsedQuotaSize)
expectedQuotaFiles = expectedQuotaFiles + 1
expectedQuotaSize = expectedQuotaSize + encryptedFileSize
err = sftpUploadFile(testFilePath, testFileName+".link", testFileSize, client)
assert.NoError(t, err)
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
assert.NoError(t, err)
assert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles)
assert.Equal(t, expectedQuotaSize, user.UsedQuotaSize)
}
// now set a quota size restriction and upload the same file, upload should fail for space limit exceeded
user.QuotaSize = encryptedFileSize*2 - 1
user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "")
assert.NoError(t, err)
conn, client, err = getSftpClient(user, usePubKey)
if assert.NoError(t, err) {
defer conn.Close()
defer client.Close()
err = sftpUploadFile(testFilePath, testFileName, testFileSize, client)
assert.Error(t, err, "quota size exceeded, file upload must fail")
err = client.Remove(testFileName)
assert.NoError(t, err)
}
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.Remove(testFilePath)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
}
func TestQuotaScanCryptFs(t *testing.T) {
usePubKey := false
user, _, err := httpdtest.AddUser(getTestUserWithCryptFs(usePubKey), http.StatusCreated)
assert.NoError(t, err)
testFileSize := int64(65535)
encryptedFileSize, err := getEncryptedFileSize(testFileSize)
assert.NoError(t, err)
expectedQuotaSize := user.UsedQuotaSize + encryptedFileSize
expectedQuotaFiles := user.UsedQuotaFiles + 1
conn, client, err := getSftpClient(user, usePubKey)
if assert.NoError(t, err) {
defer conn.Close()
defer client.Close()
testFilePath := filepath.Join(homeBasePath, testFileName)
err = createTestFile(testFilePath, testFileSize)
assert.NoError(t, err)
err = sftpUploadFile(testFilePath, testFileName, testFileSize, client)
assert.NoError(t, err)
err = os.Remove(testFilePath)
assert.NoError(t, err)
}
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
// create user with the same home dir, so there is at least an untracked file
user, _, err = httpdtest.AddUser(getTestUser(usePubKey), http.StatusCreated)
assert.NoError(t, err)
_, err = httpdtest.StartQuotaScan(user, http.StatusAccepted)
assert.NoError(t, err)
assert.Eventually(t, func() bool {
scans, _, err := httpdtest.GetQuotaScans(http.StatusOK)
if err == nil {
return len(scans) == 0
}
return false
}, 1*time.Second, 50*time.Millisecond)
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
assert.NoError(t, err)
assert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles)
assert.Equal(t, expectedQuotaSize, user.UsedQuotaSize)
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
}
func TestGetMimeTypeCryptFs(t *testing.T) {
usePubKey := true
user, _, err := httpdtest.AddUser(getTestUserWithCryptFs(usePubKey), http.StatusCreated)
assert.NoError(t, err)
conn, client, err := getSftpClient(user, usePubKey)
if assert.NoError(t, err) {
defer conn.Close()
defer client.Close()
sftpFile, err := client.OpenFile(testFileName, os.O_RDWR|os.O_CREATE|os.O_TRUNC)
if assert.NoError(t, err) {
testData := []byte("some UTF-8 text so we should get a text/plain mime type")
n, err := sftpFile.Write(testData)
assert.NoError(t, err)
assert.Equal(t, len(testData), n)
err = sftpFile.Close()
assert.NoError(t, err)
}
}
user.FsConfig.CryptConfig.Passphrase = kms.NewPlainSecret(testPassphrase)
fs, err := user.GetFilesystem("connID")
if assert.NoError(t, err) {
assert.True(t, vfs.IsCryptOsFs(fs))
mime, err := fs.GetMimeType(filepath.Join(user.GetHomeDir(), testFileName))
assert.NoError(t, err)
assert.Equal(t, "text/plain; charset=utf-8", mime)
}
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
}
func TestTruncate(t *testing.T) {
// truncate is not supported
usePubKey := true
user, _, err := httpdtest.AddUser(getTestUserWithCryptFs(usePubKey), http.StatusCreated)
assert.NoError(t, err)
conn, client, err := getSftpClient(user, usePubKey)
if assert.NoError(t, err) {
defer conn.Close()
defer client.Close()
f, err := client.OpenFile(testFileName, os.O_WRONLY|os.O_CREATE)
if assert.NoError(t, err) {
err = f.Truncate(0)
assert.NoError(t, err)
err = f.Truncate(1)
assert.Error(t, err)
}
err = f.Close()
assert.NoError(t, err)
err = client.Truncate(testFileName, 0)
assert.Error(t, err)
}
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
}
func TestSCPBasicHandlingCryptoFs(t *testing.T) {
if scpPath == "" {
t.Skip("scp command not found, unable to execute this test")
}
usePubKey := true
u := getTestUserWithCryptFs(usePubKey)
u.QuotaSize = 6553600
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
testFilePath := filepath.Join(homeBasePath, testFileName)
testFileSize := int64(131074)
encryptedFileSize, err := getEncryptedFileSize(testFileSize)
assert.NoError(t, err)
expectedQuotaSize := user.UsedQuotaSize + encryptedFileSize
expectedQuotaFiles := user.UsedQuotaFiles + 1
err = createTestFile(testFilePath, testFileSize)
assert.NoError(t, err)
remoteUpPath := fmt.Sprintf("%v@127.0.0.1:%v", user.Username, "/")
remoteDownPath := fmt.Sprintf("%v@127.0.0.1:%v", user.Username, path.Join("/", testFileName))
localPath := filepath.Join(homeBasePath, "scp_download.dat")
// test to download a missing file
err = scpDownload(localPath, remoteDownPath, false, false)
assert.Error(t, err, "downloading a missing file via scp must fail")
err = scpUpload(testFilePath, remoteUpPath, false, false)
assert.NoError(t, err)
err = scpDownload(localPath, remoteDownPath, false, false)
assert.NoError(t, err)
fi, err := os.Stat(localPath)
if assert.NoError(t, err) {
assert.Equal(t, testFileSize, fi.Size())
}
fi, err = os.Stat(filepath.Join(user.GetHomeDir(), testFileName))
if assert.NoError(t, err) {
assert.Equal(t, encryptedFileSize, fi.Size())
}
err = os.Remove(localPath)
assert.NoError(t, err)
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
assert.NoError(t, err)
assert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles)
assert.Equal(t, expectedQuotaSize, user.UsedQuotaSize)
// now overwrite the existing file
err = scpUpload(testFilePath, remoteUpPath, false, false)
assert.NoError(t, err)
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
assert.NoError(t, err)
assert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles)
assert.Equal(t, expectedQuotaSize, user.UsedQuotaSize)
assert.NoError(t, err)
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
err = os.Remove(testFilePath)
assert.NoError(t, err)
}
func TestSCPRecursiveCryptFs(t *testing.T) {
if scpPath == "" {
t.Skip("scp command not found, unable to execute this test")
}
usePubKey := true
u := getTestUserWithCryptFs(usePubKey)
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
assert.NoError(t, err)
testBaseDirName := "atestdir"
testBaseDirPath := filepath.Join(homeBasePath, testBaseDirName)
testBaseDirDownName := "test_dir_down" //nolint:goconst
testBaseDirDownPath := filepath.Join(homeBasePath, testBaseDirDownName)
testFilePath := filepath.Join(homeBasePath, testBaseDirName, testFileName)
testFilePath1 := filepath.Join(homeBasePath, testBaseDirName, testBaseDirName, testFileName)
testFileSize := int64(131074)
err = createTestFile(testFilePath, testFileSize)
assert.NoError(t, err)
err = createTestFile(testFilePath1, testFileSize)
assert.NoError(t, err)
remoteDownPath := fmt.Sprintf("%v@127.0.0.1:%v", user.Username, path.Join("/", testBaseDirName))
remoteUpPath := fmt.Sprintf("%v@127.0.0.1:%v", user.Username, "/")
err = scpUpload(testBaseDirPath, remoteUpPath, true, false)
assert.NoError(t, err)
// overwrite existing dir
err = scpUpload(testBaseDirPath, remoteUpPath, true, false)
assert.NoError(t, err)
err = scpDownload(testBaseDirDownPath, remoteDownPath, true, true)
assert.NoError(t, err)
// test download without passing -r
err = scpDownload(testBaseDirDownPath, remoteDownPath, true, false)
assert.Error(t, err, "recursive download without -r must fail")
fi, err := os.Stat(filepath.Join(testBaseDirDownPath, testFileName))
if assert.NoError(t, err) {
assert.Equal(t, testFileSize, fi.Size())
}
fi, err = os.Stat(filepath.Join(testBaseDirDownPath, testBaseDirName, testFileName))
if assert.NoError(t, err) {
assert.Equal(t, testFileSize, fi.Size())
}
// upload to a non existent dir
remoteUpPath = fmt.Sprintf("%v@127.0.0.1:%v", user.Username, "/non_existent_dir")
err = scpUpload(testBaseDirPath, remoteUpPath, true, false)
assert.Error(t, err, "uploading via scp to a non existent dir must fail")
err = os.RemoveAll(testBaseDirPath)
assert.NoError(t, err)
err = os.RemoveAll(testBaseDirDownPath)
assert.NoError(t, err)
err = os.RemoveAll(user.GetHomeDir())
assert.NoError(t, err)
_, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err)
}
func getEncryptedFileSize(size int64) (int64, error) {
encSize, err := sio.EncryptedSize(uint64(size))
return int64(encSize) + 33, err
}
func getTestUserWithCryptFs(usePubKey bool) dataprovider.User {
u := getTestUser(usePubKey)
u.FsConfig.Provider = sdk.CryptedFilesystemProvider
u.FsConfig.CryptConfig.Passphrase = kms.NewPlainSecret(testPassphrase)
return u
}