aboutsummaryrefslogtreecommitdiff
path: root/test/sftp/basic_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'test/sftp/basic_test.go')
-rw-r--r--test/sftp/basic_test.go652
1 files changed, 652 insertions, 0 deletions
diff --git a/test/sftp/basic_test.go b/test/sftp/basic_test.go
new file mode 100644
index 000000000..e5ffe90d1
--- /dev/null
+++ b/test/sftp/basic_test.go
@@ -0,0 +1,652 @@
+package sftp
+
+import (
+ "bytes"
+ "io"
+ "path"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+// TestHomeDirPathTranslation tests that SFTP operations correctly translate
+// paths relative to the user's HomeDir.
+// This is the fix for https://github.com/seaweedfs/seaweedfs/issues/7470
+func TestHomeDirPathTranslation(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping integration test in short mode")
+ }
+
+ config := DefaultTestConfig()
+ config.EnableDebug = testing.Verbose()
+
+ fw := NewSftpTestFramework(t, config)
+ err := fw.Setup(config)
+ require.NoError(t, err, "failed to setup test framework")
+ defer fw.Cleanup()
+
+ // Test with user "testuser" who has HomeDir="/sftp/testuser"
+ // When they upload to "/", it should actually go to "/sftp/testuser"
+ sftpClient, sshConn, err := fw.ConnectSFTP("testuser", "testuserpassword")
+ require.NoError(t, err, "failed to connect as testuser")
+ defer sshConn.Close()
+ defer sftpClient.Close()
+
+ // Test 1: Upload file to "/" (should map to /sftp/testuser/)
+ t.Run("UploadToRoot", func(t *testing.T) {
+ testContent := []byte("Hello from SFTP test!")
+ filename := "test_upload.txt"
+
+ // Create file at "/" from user's perspective
+ file, err := sftpClient.Create("/" + filename)
+ require.NoError(t, err, "should be able to create file at /")
+
+ _, err = file.Write(testContent)
+ require.NoError(t, err, "should be able to write to file")
+ err = file.Close()
+ require.NoError(t, err, "should be able to close file")
+
+ // Verify file exists and has correct content
+ readFile, err := sftpClient.Open("/" + filename)
+ require.NoError(t, err, "should be able to open file")
+ defer readFile.Close()
+
+ content, err := io.ReadAll(readFile)
+ require.NoError(t, err, "should be able to read file")
+ require.Equal(t, testContent, content, "file content should match")
+
+ // Clean up
+ err = sftpClient.Remove("/" + filename)
+ require.NoError(t, err, "should be able to remove file")
+ })
+
+ // Test 2: Create directory at "/" (should map to /sftp/testuser/)
+ t.Run("CreateDirAtRoot", func(t *testing.T) {
+ dirname := "test_dir"
+
+ err := sftpClient.Mkdir("/" + dirname)
+ require.NoError(t, err, "should be able to create directory at /")
+
+ // Verify directory exists
+ info, err := sftpClient.Stat("/" + dirname)
+ require.NoError(t, err, "should be able to stat directory")
+ require.True(t, info.IsDir(), "should be a directory")
+
+ // Clean up
+ err = sftpClient.RemoveDirectory("/" + dirname)
+ require.NoError(t, err, "should be able to remove directory")
+ })
+
+ // Test 3: List directory at "/" (should list /sftp/testuser/)
+ t.Run("ListRoot", func(t *testing.T) {
+ // Create a test file first
+ testContent := []byte("list test content")
+ filename := "list_test.txt"
+
+ file, err := sftpClient.Create("/" + filename)
+ require.NoError(t, err)
+ _, err = file.Write(testContent)
+ require.NoError(t, err)
+ file.Close()
+
+ // List root directory
+ files, err := sftpClient.ReadDir("/")
+ require.NoError(t, err, "should be able to list root directory")
+
+ // Should find our test file
+ found := false
+ for _, f := range files {
+ if f.Name() == filename {
+ found = true
+ break
+ }
+ }
+ require.True(t, found, "should find test file in listing")
+
+ // Clean up
+ err = sftpClient.Remove("/" + filename)
+ require.NoError(t, err)
+ })
+
+ // Test 4: Nested directory operations
+ t.Run("NestedOperations", func(t *testing.T) {
+ // Create nested directory structure
+ err := sftpClient.MkdirAll("/nested/dir/structure")
+ require.NoError(t, err, "should be able to create nested directories")
+
+ // Create file in nested directory
+ testContent := []byte("nested file content")
+ file, err := sftpClient.Create("/nested/dir/structure/file.txt")
+ require.NoError(t, err)
+ _, err = file.Write(testContent)
+ require.NoError(t, err)
+ file.Close()
+
+ // Verify file exists
+ readFile, err := sftpClient.Open("/nested/dir/structure/file.txt")
+ require.NoError(t, err)
+ content, err := io.ReadAll(readFile)
+ require.NoError(t, err)
+ readFile.Close()
+ require.Equal(t, testContent, content)
+
+ // Clean up
+ err = sftpClient.Remove("/nested/dir/structure/file.txt")
+ require.NoError(t, err)
+ err = sftpClient.RemoveDirectory("/nested/dir/structure")
+ require.NoError(t, err)
+ err = sftpClient.RemoveDirectory("/nested/dir")
+ require.NoError(t, err)
+ err = sftpClient.RemoveDirectory("/nested")
+ require.NoError(t, err)
+ })
+
+ // Test 5: Rename operation
+ t.Run("RenameFile", func(t *testing.T) {
+ testContent := []byte("rename test content")
+
+ file, err := sftpClient.Create("/original.txt")
+ require.NoError(t, err)
+ _, err = file.Write(testContent)
+ require.NoError(t, err)
+ file.Close()
+
+ // Rename file
+ err = sftpClient.Rename("/original.txt", "/renamed.txt")
+ require.NoError(t, err, "should be able to rename file")
+
+ // Verify old file doesn't exist
+ _, err = sftpClient.Stat("/original.txt")
+ require.Error(t, err, "original file should not exist")
+
+ // Verify new file exists with correct content
+ readFile, err := sftpClient.Open("/renamed.txt")
+ require.NoError(t, err, "renamed file should exist")
+ content, err := io.ReadAll(readFile)
+ require.NoError(t, err)
+ readFile.Close()
+ require.Equal(t, testContent, content)
+
+ // Clean up
+ err = sftpClient.Remove("/renamed.txt")
+ require.NoError(t, err)
+ })
+}
+
+// TestAdminRootAccess tests that admin user with HomeDir="/" can access everything
+func TestAdminRootAccess(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping integration test in short mode")
+ }
+
+ config := DefaultTestConfig()
+ config.EnableDebug = testing.Verbose()
+
+ fw := NewSftpTestFramework(t, config)
+ err := fw.Setup(config)
+ require.NoError(t, err, "failed to setup test framework")
+ defer fw.Cleanup()
+
+ // Connect as admin with HomeDir="/"
+ sftpClient, sshConn, err := fw.ConnectSFTP("admin", "adminpassword")
+ require.NoError(t, err, "failed to connect as admin")
+ defer sshConn.Close()
+ defer sftpClient.Close()
+
+ // Admin should be able to create directories anywhere
+ t.Run("CreateAnyDirectory", func(t *testing.T) {
+ // Create the user's home directory structure
+ err := sftpClient.MkdirAll("/sftp/testuser")
+ require.NoError(t, err, "admin should be able to create any directory")
+
+ // Create file in that directory
+ testContent := []byte("admin created this")
+ file, err := sftpClient.Create("/sftp/testuser/admin_file.txt")
+ require.NoError(t, err)
+ _, err = file.Write(testContent)
+ require.NoError(t, err)
+ file.Close()
+
+ // Verify file exists
+ info, err := sftpClient.Stat("/sftp/testuser/admin_file.txt")
+ require.NoError(t, err)
+ require.False(t, info.IsDir())
+
+ // Clean up
+ err = sftpClient.Remove("/sftp/testuser/admin_file.txt")
+ require.NoError(t, err)
+ })
+}
+
+// TestLargeFileUpload tests uploading larger files through SFTP
+func TestLargeFileUpload(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping integration test in short mode")
+ }
+
+ config := DefaultTestConfig()
+ config.EnableDebug = testing.Verbose()
+
+ fw := NewSftpTestFramework(t, config)
+ err := fw.Setup(config)
+ require.NoError(t, err, "failed to setup test framework")
+ defer fw.Cleanup()
+
+ sftpClient, sshConn, err := fw.ConnectSFTP("testuser", "testuserpassword")
+ require.NoError(t, err, "failed to connect as testuser")
+ defer sshConn.Close()
+ defer sftpClient.Close()
+
+ // Create a 1MB file
+ t.Run("Upload1MB", func(t *testing.T) {
+ size := 1024 * 1024 // 1MB
+ testData := bytes.Repeat([]byte("A"), size)
+
+ file, err := sftpClient.Create("/large_file.bin")
+ require.NoError(t, err)
+ n, err := file.Write(testData)
+ require.NoError(t, err)
+ require.Equal(t, size, n)
+ file.Close()
+
+ // Verify file size
+ info, err := sftpClient.Stat("/large_file.bin")
+ require.NoError(t, err)
+ require.Equal(t, int64(size), info.Size())
+
+ // Verify content
+ readFile, err := sftpClient.Open("/large_file.bin")
+ require.NoError(t, err)
+ content, err := io.ReadAll(readFile)
+ require.NoError(t, err)
+ readFile.Close()
+ require.Equal(t, testData, content)
+
+ // Clean up
+ err = sftpClient.Remove("/large_file.bin")
+ require.NoError(t, err)
+ })
+}
+
+// TestStatOperations tests Stat and Lstat operations
+func TestStatOperations(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping integration test in short mode")
+ }
+
+ config := DefaultTestConfig()
+ config.EnableDebug = testing.Verbose()
+
+ fw := NewSftpTestFramework(t, config)
+ err := fw.Setup(config)
+ require.NoError(t, err, "failed to setup test framework")
+ defer fw.Cleanup()
+
+ sftpClient, sshConn, err := fw.ConnectSFTP("testuser", "testuserpassword")
+ require.NoError(t, err, "failed to connect as testuser")
+ defer sshConn.Close()
+ defer sftpClient.Close()
+
+ // Create a test file
+ testContent := []byte("stat test content")
+ file, err := sftpClient.Create("/stat_test.txt")
+ require.NoError(t, err)
+ _, err = file.Write(testContent)
+ require.NoError(t, err)
+ file.Close()
+
+ t.Run("StatFile", func(t *testing.T) {
+ info, err := sftpClient.Stat("/stat_test.txt")
+ require.NoError(t, err)
+ require.Equal(t, "stat_test.txt", info.Name())
+ require.Equal(t, int64(len(testContent)), info.Size())
+ require.False(t, info.IsDir())
+ })
+
+ t.Run("StatDirectory", func(t *testing.T) {
+ err := sftpClient.Mkdir("/stat_dir")
+ require.NoError(t, err)
+
+ info, err := sftpClient.Stat("/stat_dir")
+ require.NoError(t, err)
+ require.Equal(t, "stat_dir", info.Name())
+ require.True(t, info.IsDir())
+
+ // Clean up
+ err = sftpClient.RemoveDirectory("/stat_dir")
+ require.NoError(t, err)
+ })
+
+ t.Run("StatRoot", func(t *testing.T) {
+ // Should be able to stat "/" which maps to user's home directory
+ info, err := sftpClient.Stat("/")
+ require.NoError(t, err, "should be able to stat root (home) directory")
+ require.True(t, info.IsDir(), "root should be a directory")
+ })
+
+ // Clean up
+ err = sftpClient.Remove("/stat_test.txt")
+ require.NoError(t, err)
+}
+
+// TestWalk tests walking directory trees
+func TestWalk(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping integration test in short mode")
+ }
+
+ config := DefaultTestConfig()
+ config.EnableDebug = testing.Verbose()
+
+ fw := NewSftpTestFramework(t, config)
+ err := fw.Setup(config)
+ require.NoError(t, err, "failed to setup test framework")
+ defer fw.Cleanup()
+
+ sftpClient, sshConn, err := fw.ConnectSFTP("testuser", "testuserpassword")
+ require.NoError(t, err, "failed to connect as testuser")
+ defer sshConn.Close()
+ defer sftpClient.Close()
+
+ // Create directory structure
+ err = sftpClient.MkdirAll("/walk/a/b")
+ require.NoError(t, err)
+ err = sftpClient.MkdirAll("/walk/c")
+ require.NoError(t, err)
+
+ // Create files
+ for _, p := range []string{"/walk/file1.txt", "/walk/a/file2.txt", "/walk/a/b/file3.txt", "/walk/c/file4.txt"} {
+ file, err := sftpClient.Create(p)
+ require.NoError(t, err)
+ file.Write([]byte("test"))
+ file.Close()
+ }
+
+ t.Run("WalkEntireTree", func(t *testing.T) {
+ var paths []string
+ walker := sftpClient.Walk("/walk")
+ for walker.Step() {
+ if walker.Err() != nil {
+ continue
+ }
+ paths = append(paths, walker.Path())
+ }
+
+ // Should find all directories and files
+ require.Contains(t, paths, "/walk")
+ require.Contains(t, paths, "/walk/a")
+ require.Contains(t, paths, "/walk/a/b")
+ require.Contains(t, paths, "/walk/c")
+ })
+
+ // Clean up
+ for _, p := range []string{"/walk/file1.txt", "/walk/a/file2.txt", "/walk/a/b/file3.txt", "/walk/c/file4.txt"} {
+ require.NoError(t, sftpClient.Remove(p))
+ }
+ for _, p := range []string{"/walk/a/b", "/walk/a", "/walk/c", "/walk"} {
+ require.NoError(t, sftpClient.RemoveDirectory(p))
+ }
+}
+
+// TestCurrentWorkingDirectory tests that Getwd and Chdir work correctly
+func TestCurrentWorkingDirectory(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping integration test in short mode")
+ }
+
+ config := DefaultTestConfig()
+ config.EnableDebug = testing.Verbose()
+
+ fw := NewSftpTestFramework(t, config)
+ err := fw.Setup(config)
+ require.NoError(t, err, "failed to setup test framework")
+ defer fw.Cleanup()
+
+ sftpClient, sshConn, err := fw.ConnectSFTP("testuser", "testuserpassword")
+ require.NoError(t, err, "failed to connect as testuser")
+ defer sshConn.Close()
+ defer sftpClient.Close()
+
+ // Create test directory
+ err = sftpClient.Mkdir("/cwd_test")
+ require.NoError(t, err)
+
+ t.Run("GetCurrentDir", func(t *testing.T) {
+ cwd, err := sftpClient.Getwd()
+ require.NoError(t, err)
+ // The initial working directory should be the user's home directory
+ // which from the user's perspective is "/"
+ require.Equal(t, "/", cwd, "initial working directory should be the virtual root")
+ })
+
+ t.Run("ChangeAndCreate", func(t *testing.T) {
+ // Create file in subdirectory using relative path after chdir
+ // Note: pkg/sftp doesn't support Chdir, so we test using absolute paths
+ file, err := sftpClient.Create("/cwd_test/relative_file.txt")
+ require.NoError(t, err)
+ file.Write([]byte("test"))
+ file.Close()
+
+ // Verify using absolute path
+ _, err = sftpClient.Stat("/cwd_test/relative_file.txt")
+ require.NoError(t, err)
+
+ // Clean up
+ sftpClient.Remove("/cwd_test/relative_file.txt")
+ })
+
+ // Clean up
+ err = sftpClient.RemoveDirectory("/cwd_test")
+ require.NoError(t, err)
+}
+
+// TestPathEdgeCases tests various edge cases in path handling
+func TestPathEdgeCases(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping integration test in short mode")
+ }
+
+ config := DefaultTestConfig()
+ config.EnableDebug = testing.Verbose()
+
+ fw := NewSftpTestFramework(t, config)
+ err := fw.Setup(config)
+ require.NoError(t, err, "failed to setup test framework")
+ defer fw.Cleanup()
+
+ sftpClient, sshConn, err := fw.ConnectSFTP("testuser", "testuserpassword")
+ require.NoError(t, err, "failed to connect as testuser")
+ defer sshConn.Close()
+ defer sftpClient.Close()
+
+ t.Run("PathWithDotDot", func(t *testing.T) {
+ // Create directory structure
+ err := sftpClient.MkdirAll("/edge/subdir")
+ require.NoError(t, err)
+
+ // Create file using path with ..
+ file, err := sftpClient.Create("/edge/subdir/../file.txt")
+ require.NoError(t, err)
+ file.Write([]byte("test"))
+ file.Close()
+
+ // Verify file was created in /edge
+ _, err = sftpClient.Stat("/edge/file.txt")
+ require.NoError(t, err, "file should be created in parent directory")
+
+ // Clean up
+ sftpClient.Remove("/edge/file.txt")
+ sftpClient.RemoveDirectory("/edge/subdir")
+ sftpClient.RemoveDirectory("/edge")
+ })
+
+ t.Run("PathWithTrailingSlash", func(t *testing.T) {
+ err := sftpClient.Mkdir("/trailing")
+ require.NoError(t, err)
+
+ // Stat with trailing slash
+ info, err := sftpClient.Stat("/trailing/")
+ require.NoError(t, err)
+ require.True(t, info.IsDir())
+
+ // Clean up
+ sftpClient.RemoveDirectory("/trailing")
+ })
+
+ t.Run("CreateFileAtRootPath", func(t *testing.T) {
+ // This is the exact scenario from issue #7470
+ // User with HomeDir="/sftp/testuser" uploads to "/"
+ file, err := sftpClient.Create("/issue7470.txt")
+ require.NoError(t, err, "should be able to create file at / (issue #7470)")
+ file.Write([]byte("This tests the fix for issue #7470"))
+ file.Close()
+
+ // Verify
+ _, err = sftpClient.Stat("/issue7470.txt")
+ require.NoError(t, err)
+
+ // Clean up
+ sftpClient.Remove("/issue7470.txt")
+ })
+
+ // Security test: path traversal attacks should be blocked
+ t.Run("PathTraversalPrevention", func(t *testing.T) {
+ // User's HomeDir is "/sftp/testuser"
+ // Attempting to escape via "../.." should NOT create files outside home directory
+
+ // First, create a valid file to ensure we can write
+ validFile, err := sftpClient.Create("/valid.txt")
+ require.NoError(t, err)
+ validFile.Write([]byte("valid"))
+ validFile.Close()
+
+ // Try various path traversal attempts
+ // These should either:
+ // 1. Be blocked (error returned), OR
+ // 2. Be safely resolved to stay within home directory
+
+ traversalPaths := []string{
+ "/../escape.txt",
+ "/../../escape.txt",
+ "/../../../escape.txt",
+ "/subdir/../../escape.txt",
+ "/./../../escape.txt",
+ }
+
+ for _, traversalPath := range traversalPaths {
+ t.Run(traversalPath, func(t *testing.T) {
+ // Note: The pkg/sftp client sanitizes paths locally before sending them to the server.
+ // So "/../escape.txt" becomes "/escape.txt" on the wire.
+ // Therefore, we cannot trigger the server-side path traversal block with this client.
+ // Instead, we verify that the file is created successfully within the jail (contained).
+ // The server-side protection logic is verified in unit tests (sftpd/sftp_server_test.go).
+
+ file, err := sftpClient.Create(traversalPath)
+ require.NoError(t, err, "creation should succeed because client sanitizes path")
+ file.Close()
+
+ // Clean up
+ err = sftpClient.Remove(traversalPath)
+ require.NoError(t, err)
+ })
+ }
+
+ // Clean up
+ sftpClient.Remove("/valid.txt")
+ })
+}
+
+// TestFileContent tests reading and writing file content correctly
+func TestFileContent(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping integration test in short mode")
+ }
+
+ config := DefaultTestConfig()
+ config.EnableDebug = testing.Verbose()
+
+ fw := NewSftpTestFramework(t, config)
+ err := fw.Setup(config)
+ require.NoError(t, err, "failed to setup test framework")
+ defer fw.Cleanup()
+
+ sftpClient, sshConn, err := fw.ConnectSFTP("testuser", "testuserpassword")
+ require.NoError(t, err, "failed to connect as testuser")
+ defer sshConn.Close()
+ defer sftpClient.Close()
+
+ t.Run("BinaryContent", func(t *testing.T) {
+ // Create binary data with all byte values
+ data := make([]byte, 256)
+ for i := 0; i < 256; i++ {
+ data[i] = byte(i)
+ }
+
+ file, err := sftpClient.Create("/binary.bin")
+ require.NoError(t, err)
+ n, err := file.Write(data)
+ require.NoError(t, err)
+ require.Equal(t, 256, n)
+ file.Close()
+
+ // Read back
+ readFile, err := sftpClient.Open("/binary.bin")
+ require.NoError(t, err)
+ content, err := io.ReadAll(readFile)
+ require.NoError(t, err)
+ readFile.Close()
+
+ require.Equal(t, data, content, "binary content should match")
+
+ // Clean up
+ sftpClient.Remove("/binary.bin")
+ })
+
+ t.Run("EmptyFile", func(t *testing.T) {
+ file, err := sftpClient.Create("/empty.txt")
+ require.NoError(t, err)
+ file.Close()
+
+ info, err := sftpClient.Stat("/empty.txt")
+ require.NoError(t, err)
+ require.Equal(t, int64(0), info.Size())
+
+ // Clean up
+ sftpClient.Remove("/empty.txt")
+ })
+
+ t.Run("UnicodeFilename", func(t *testing.T) {
+ filename := "/文件名.txt"
+ content := []byte("Unicode content: 你好世界")
+
+ file, err := sftpClient.Create(filename)
+ require.NoError(t, err)
+ file.Write(content)
+ file.Close()
+
+ // Read back
+ readFile, err := sftpClient.Open(filename)
+ require.NoError(t, err)
+ readContent, err := io.ReadAll(readFile)
+ require.NoError(t, err)
+ readFile.Close()
+
+ require.Equal(t, content, readContent)
+
+ // Verify in listing
+ files, err := sftpClient.ReadDir("/")
+ require.NoError(t, err)
+ found := false
+ for _, f := range files {
+ if f.Name() == path.Base(filename) {
+ found = true
+ break
+ }
+ }
+ require.True(t, found, "should find unicode filename in listing")
+
+ // Clean up
+ sftpClient.Remove(filename)
+ })
+}
+