// 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 . package ftpd_test import ( "crypto/sha256" "fmt" "hash" "io" "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/common" "github.com/drakkan/sftpgo/v2/internal/dataprovider" "github.com/drakkan/sftpgo/v2/internal/httpdtest" "github.com/drakkan/sftpgo/v2/internal/kms" ) func TestBasicFTPHandlingCryptFs(t *testing.T) { u := getTestUserWithCryptFs() u.QuotaSize = 6553600 user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) client, err := getFTPClient(user, true, nil) if assert.NoError(t, err) { assert.Len(t, common.Connections.GetStats(""), 1) testFilePath := filepath.Join(homeBasePath, testFileName) testFileSize := int64(65535) encryptedFileSize, err := getEncryptedFileSize(testFileSize) assert.NoError(t, err) expectedQuotaSize := encryptedFileSize expectedQuotaFiles := 1 err = createTestFile(testFilePath, testFileSize) assert.NoError(t, err) err = checkBasicFTP(client) assert.NoError(t, err) err = ftpUploadFile(testFilePath, path.Join("/missing_dir", testFileName), testFileSize, client, 0) assert.Error(t, err) err = ftpUploadFile(testFilePath, testFileName, testFileSize, client, 0) assert.NoError(t, err) // overwrite an existing file err = ftpUploadFile(testFilePath, testFileName, testFileSize, client, 0) assert.NoError(t, err) localDownloadPath := filepath.Join(homeBasePath, testDLFileName) err = ftpDownloadFile(testFileName, localDownloadPath, testFileSize, client, 0) assert.NoError(t, err) info, err := os.Stat(localDownloadPath) if assert.NoError(t, err) { assert.Equal(t, testFileSize, info.Size()) } list, err := client.List(".") if assert.NoError(t, err) { if assert.Len(t, list, 1) { assert.Equal(t, testFileSize, int64(list[0].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) err = client.Rename(testFileName, testFileName+"1") assert.NoError(t, err) err = client.Delete(testFileName) assert.Error(t, err) err = client.Delete(testFileName + "1") assert.NoError(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) curDir, err := client.CurrentDir() if assert.NoError(t, err) { assert.Equal(t, "/", curDir) } testDir := "testDir" err = client.MakeDir(testDir) assert.NoError(t, err) err = client.ChangeDir(testDir) assert.NoError(t, err) curDir, err = client.CurrentDir() if assert.NoError(t, err) { assert.Equal(t, path.Join("/", testDir), curDir) } err = ftpUploadFile(testFilePath, testFileName, testFileSize, client, 0) assert.NoError(t, err) size, err := client.FileSize(path.Join("/", testDir, testFileName)) assert.NoError(t, err) assert.Equal(t, testFileSize, size) err = client.ChangeDirToParent() assert.NoError(t, err) curDir, err = client.CurrentDir() if assert.NoError(t, err) { assert.Equal(t, "/", curDir) } err = client.Delete(path.Join("/", testDir, testFileName)) assert.NoError(t, err) err = client.Delete(testDir) assert.Error(t, err) err = client.RemoveDir(testDir) assert.NoError(t, err) err = os.Remove(testFilePath) assert.NoError(t, err) err = os.Remove(localDownloadPath) assert.NoError(t, err) err = client.Quit() assert.NoError(t, err) } _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) assert.Eventually(t, func() bool { return len(common.Connections.GetStats("")) == 0 }, 1*time.Second, 50*time.Millisecond) assert.Eventually(t, func() bool { return common.Connections.GetClientConnections() == 0 }, 1000*time.Millisecond, 50*time.Millisecond) assert.Equal(t, int32(0), common.Connections.GetTotalTransfers()) } func TestBufferedCryptFs(t *testing.T) { u := getTestUserWithCryptFs() u.FsConfig.CryptConfig.OSFsConfig = sdk.OSFsConfig{ ReadBufferSize: 1, WriteBufferSize: 1, } user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) client, err := getFTPClient(user, true, nil) if assert.NoError(t, err) { testFilePath := filepath.Join(homeBasePath, testFileName) testFileSize := int64(65535) err = createTestFile(testFilePath, testFileSize) assert.NoError(t, err) err = checkBasicFTP(client) assert.NoError(t, err) err = ftpUploadFile(testFilePath, testFileName, testFileSize, client, 0) assert.NoError(t, err) // overwrite an existing file err = ftpUploadFile(testFilePath, testFileName, testFileSize, client, 0) assert.NoError(t, err) localDownloadPath := filepath.Join(homeBasePath, testDLFileName) err = ftpDownloadFile(testFileName, localDownloadPath, testFileSize, client, 0) assert.NoError(t, err) info, err := os.Stat(localDownloadPath) if assert.NoError(t, err) { assert.Equal(t, testFileSize, info.Size()) } err = os.Remove(testFilePath) assert.NoError(t, err) err = os.Remove(localDownloadPath) assert.NoError(t, err) err = client.Quit() assert.NoError(t, err) } _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) assert.Eventually(t, func() bool { return len(common.Connections.GetStats("")) == 0 }, 1*time.Second, 50*time.Millisecond) assert.Eventually(t, func() bool { return common.Connections.GetClientConnections() == 0 }, 1000*time.Millisecond, 50*time.Millisecond) assert.Equal(t, int32(0), common.Connections.GetTotalTransfers()) } func TestZeroBytesTransfersCryptFs(t *testing.T) { u := getTestUserWithCryptFs() user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) client, err := getFTPClient(user, true, nil) if assert.NoError(t, err) { testFileName := "testfilename" err = checkBasicFTP(client) assert.NoError(t, err) localDownloadPath := filepath.Join(homeBasePath, "emptydownload") err = os.WriteFile(localDownloadPath, []byte(""), os.ModePerm) assert.NoError(t, err) err = ftpUploadFile(localDownloadPath, testFileName, 0, client, 0) assert.NoError(t, err) size, err := client.FileSize(testFileName) assert.NoError(t, err) assert.Equal(t, int64(0), size) err = os.Remove(localDownloadPath) assert.NoError(t, err) assert.NoFileExists(t, localDownloadPath) err = ftpDownloadFile(testFileName, localDownloadPath, 0, client, 0) assert.NoError(t, err) info, err := os.Stat(localDownloadPath) if assert.NoError(t, err) { assert.Equal(t, int64(0), info.Size()) } err = client.Quit() 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 TestResumeCryptFs(t *testing.T) { u := getTestUserWithCryptFs() user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) client, err := getFTPClient(user, true, nil) if assert.NoError(t, err) { testFilePath := filepath.Join(homeBasePath, testFileName) data := []byte("test data") err = os.WriteFile(testFilePath, data, os.ModePerm) assert.NoError(t, err) err = ftpUploadFile(testFilePath, testFileName, int64(len(data)), client, 0) assert.NoError(t, err) // resuming uploads is not supported err = ftpUploadFile(testFilePath, testFileName, int64(len(data)+5), client, 5) assert.Error(t, err) localDownloadPath := filepath.Join(homeBasePath, testDLFileName) err = ftpDownloadFile(testFileName, localDownloadPath, int64(4), client, 5) assert.NoError(t, err) readed, err := os.ReadFile(localDownloadPath) assert.NoError(t, err) assert.Equal(t, data[5:], readed) err = ftpDownloadFile(testFileName, localDownloadPath, int64(8), client, 1) assert.NoError(t, err) readed, err = os.ReadFile(localDownloadPath) assert.NoError(t, err) assert.Equal(t, data[1:], readed) err = ftpDownloadFile(testFileName, localDownloadPath, int64(0), client, 9) assert.NoError(t, err) err = client.Delete(testFileName) assert.NoError(t, err) err = ftpUploadFile(testFilePath, testFileName, int64(len(data)), client, 0) assert.NoError(t, err) // now append to a file srcFile, err := os.Open(testFilePath) if assert.NoError(t, err) { err = client.Append(testFileName, srcFile) assert.Error(t, err) err = srcFile.Close() assert.NoError(t, err) size, err := client.FileSize(testFileName) assert.NoError(t, err) assert.Equal(t, int64(len(data)), size) err = ftpDownloadFile(testFileName, localDownloadPath, int64(len(data)), client, 0) assert.NoError(t, err) readed, err = os.ReadFile(localDownloadPath) assert.NoError(t, err) assert.Equal(t, data, readed) } // now test a download resume using a bigger file testFileSize := int64(655352) err = createTestFile(testFilePath, testFileSize) assert.NoError(t, err) initialHash, err := computeHashForFile(sha256.New(), testFilePath) assert.NoError(t, err) err = ftpUploadFile(testFilePath, testFileName, testFileSize, client, 0) assert.NoError(t, err) err = ftpDownloadFile(testFileName, localDownloadPath, testFileSize, client, 0) assert.NoError(t, err) downloadHash, err := computeHashForFile(sha256.New(), localDownloadPath) assert.NoError(t, err) assert.Equal(t, initialHash, downloadHash) err = os.Truncate(localDownloadPath, 32767) assert.NoError(t, err) err = ftpDownloadFile(testFileName, localDownloadPath+"_partial", testFileSize-32767, client, 32767) //nolint:goconst assert.NoError(t, err) file, err := os.OpenFile(localDownloadPath, os.O_APPEND|os.O_WRONLY, os.ModePerm) assert.NoError(t, err) file1, err := os.Open(localDownloadPath + "_partial") //nolint:goconst assert.NoError(t, err) _, err = io.Copy(file, file1) assert.NoError(t, err) err = file.Close() assert.NoError(t, err) err = file1.Close() assert.NoError(t, err) downloadHash, err = computeHashForFile(sha256.New(), localDownloadPath) assert.NoError(t, err) assert.Equal(t, initialHash, downloadHash) err = client.Quit() assert.NoError(t, err) err = os.Remove(testFilePath) assert.NoError(t, err) err = os.Remove(localDownloadPath) assert.NoError(t, err) err = os.Remove(localDownloadPath + "_partial") assert.NoError(t, err) } _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) err = os.RemoveAll(user.GetHomeDir()) assert.NoError(t, err) } func getTestUserWithCryptFs() dataprovider.User { user := getTestUser() user.FsConfig.Provider = sdk.CryptedFilesystemProvider user.FsConfig.CryptConfig.Passphrase = kms.NewPlainSecret("testPassphrase") return user } func getEncryptedFileSize(size int64) (int64, error) { encSize, err := sio.EncryptedSize(uint64(size)) return int64(encSize) + 33, err } func computeHashForFile(hasher hash.Hash, path string) (string, error) { hash := "" f, err := os.Open(path) if err != nil { return hash, err } defer f.Close() _, err = io.Copy(hasher, f) if err == nil { hash = fmt.Sprintf("%x", hasher.Sum(nil)) } return hash, err }