From a26962f36723c478fe13116a9d527074e92d25b1 Mon Sep 17 00:00:00 2001 From: Nicola Murino Date: Sat, 31 Jul 2021 09:42:23 +0200 Subject: [PATCH] add dot and dot dot directories to sftp/ftp file listing --- docs/full-configuration.md | 2 +- ftpd/cryptfs_test.go | 5 +++-- ftpd/ftpd_test.go | 12 ++++++++++++ ftpd/handler.go | 11 ++++++++++- sftpd/handler.go | 15 +++++++++++---- sftpd/middleware.go | 11 ++++++++--- sftpd/middleware_test.go | 26 +++++++++++++++----------- sftpd/server.go | 2 ++ util/util.go | 9 +++++++++ 9 files changed, 71 insertions(+), 22 deletions(-) diff --git a/docs/full-configuration.md b/docs/full-configuration.md index c0b0a9ed..3f697de1 100644 --- a/docs/full-configuration.md +++ b/docs/full-configuration.md @@ -106,7 +106,7 @@ The configuration file contains the following sections: - `enabled_ssh_commands`, list of enabled SSH commands. `*` enables all supported commands. More information can be found [here](./ssh-commands.md). - `keyboard_interactive_auth_hook`, string. Absolute path to an external program or an HTTP URL to invoke for keyboard interactive authentication. See [Keyboard Interactive Authentication](./keyboard-interactive.md) for more details. - `password_authentication`, boolean. Set to false to disable password authentication. This setting will disable multi-step authentication method using public key + password too. It is useful for public key only configurations if you need to manage old clients that will not attempt to authenticate with public keys if the password login method is advertised. Default: true. - - `folder_prefix`, string. Virtual root folder prefix to include in all file operations (ex: `/files`). The virtual paths used for per-directory permissions, file patterns etc. must not include the folder prefix. The prefix is only applied to SFTP requests, SCP and other SSH commands will be automatically disabled if you configure a prefix. This setting can help some specific migrations from SFTP servers based on OpenSSH and it is not recommended for general usage. Default: empty. + - `folder_prefix`, string. Virtual root folder prefix to include in all file operations (ex: `/files`). The virtual paths used for per-directory permissions, file patterns etc. must not include the folder prefix. The prefix is only applied to SFTP requests (in SFTP server mode), SCP and other SSH commands will be automatically disabled if you configure a prefix. The prefix is ignored while running as OpenSSH's SFTP subsystem. This setting can help some specific migrations from SFTP servers based on OpenSSH and it is not recommended for general usage. Default: empty. - **"ftpd"**, the configuration for the FTP server - `bindings`, list of structs. Each struct has the following fields: - `port`, integer. The port used for serving FTP requests. 0 means disabled. Default: 0. diff --git a/ftpd/cryptfs_test.go b/ftpd/cryptfs_test.go index 6a8037b4..4667411c 100644 --- a/ftpd/cryptfs_test.go +++ b/ftpd/cryptfs_test.go @@ -57,8 +57,9 @@ func TestBasicFTPHandlingCryptFs(t *testing.T) { } list, err := client.List(".") if assert.NoError(t, err) { - assert.Len(t, list, 1) - assert.Equal(t, testFileSize, int64(list[0].Size)) + assert.Len(t, list, 2) + assert.Equal(t, ".", list[0].Name) + assert.Equal(t, testFileSize, int64(list[1].Size)) } user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) assert.NoError(t, err) diff --git a/ftpd/ftpd_test.go b/ftpd/ftpd_test.go index de6a78f7..50a753e9 100644 --- a/ftpd/ftpd_test.go +++ b/ftpd/ftpd_test.go @@ -536,6 +536,18 @@ func TestBasicFTPHandling(t *testing.T) { if assert.NoError(t, err) { assert.Equal(t, path.Join("/", testDir), curDir) } + res, err := client.List(path.Join("/", testDir)) + assert.NoError(t, err) + if assert.Len(t, res, 2) { + assert.Equal(t, ".", res[0].Name) + assert.Equal(t, "..", res[1].Name) + } + res, err = client.List(path.Join("/")) + assert.NoError(t, err) + if assert.Len(t, res, 2) { + assert.Equal(t, ".", res[0].Name) + assert.Equal(t, testDir, res[1].Name) + } err = ftpUploadFile(testFilePath, testFileName, testFileSize, client, 0) assert.NoError(t, err) size, err := client.FileSize(path.Join("/", testDir, testFileName)) diff --git a/ftpd/handler.go b/ftpd/handler.go index 629ae52c..510bb441 100644 --- a/ftpd/handler.go +++ b/ftpd/handler.go @@ -14,6 +14,7 @@ import ( "github.com/drakkan/sftpgo/v2/common" "github.com/drakkan/sftpgo/v2/dataprovider" "github.com/drakkan/sftpgo/v2/logger" + "github.com/drakkan/sftpgo/v2/util" "github.com/drakkan/sftpgo/v2/vfs" ) @@ -271,7 +272,15 @@ func (c *Connection) Symlink(oldname, newname string) error { func (c *Connection) ReadDir(name string) ([]os.FileInfo, error) { c.UpdateLastActivity() - return c.ListDir(name) + files, err := c.ListDir(name) + if err != nil { + return files, err + } + if name != "/" { + files = util.PrependFileInfo(files, vfs.NewFileInfo("..", true, 0, time.Now(), false)) + } + files = util.PrependFileInfo(files, vfs.NewFileInfo(".", true, 0, time.Now(), false)) + return files, nil } // GetHandle implements ClientDriverExtentionFileTransfer diff --git a/sftpd/handler.go b/sftpd/handler.go index 300446b6..c57adf66 100644 --- a/sftpd/handler.go +++ b/sftpd/handler.go @@ -12,6 +12,7 @@ import ( "github.com/drakkan/sftpgo/v2/common" "github.com/drakkan/sftpgo/v2/dataprovider" "github.com/drakkan/sftpgo/v2/logger" + "github.com/drakkan/sftpgo/v2/util" "github.com/drakkan/sftpgo/v2/vfs" ) @@ -21,10 +22,11 @@ type Connection struct { // client's version string ClientVersion string // Remote address for this connection - RemoteAddr net.Addr - LocalAddr net.Addr - channel io.ReadWriteCloser - command string + RemoteAddr net.Addr + LocalAddr net.Addr + channel io.ReadWriteCloser + command string + folderPrefix string } // GetClientVersion returns the connected client's version @@ -201,6 +203,11 @@ func (c *Connection) Filelist(request *sftp.Request) (sftp.ListerAt, error) { if err != nil { return nil, err } + now := time.Now() + if request.Filepath != "/" || c.folderPrefix != "" { + files = util.PrependFileInfo(files, vfs.NewFileInfo("..", true, 0, now, false)) + } + files = util.PrependFileInfo(files, vfs.NewFileInfo(".", true, 0, now, false)) return listerAt(files), nil case "Stat": if !c.User.HasPerm(dataprovider.PermListItems, path.Dir(request.Filepath)) { diff --git a/sftpd/middleware.go b/sftpd/middleware.go index 331e6bb1..20cab5d1 100644 --- a/sftpd/middleware.go +++ b/sftpd/middleware.go @@ -78,10 +78,15 @@ func (p *prefixMiddleware) Filelist(request *sftp.Request) (sftp.ListerAt, error case pathIsPrefixParent: switch request.Method { case methodList: + now := time.Now() fileName := p.nextListFolder(request.Filepath) - return listerAt([]os.FileInfo{ - vfs.NewFileInfo(fileName, true, 0, time.Now(), false), - }), nil + files := make([]os.FileInfo, 0, 3) + files = append(files, vfs.NewFileInfo(".", true, 0, now, false)) + if request.Filepath != "/" { + files = append(files, vfs.NewFileInfo("..", true, 0, now, false)) + } + files = append(files, vfs.NewFileInfo(fileName, true, 0, now, false)) + return listerAt(files), nil case methodStat: return listerAt([]os.FileInfo{ vfs.NewFileInfo(request.Filepath, true, 0, time.Now(), false), diff --git a/sftpd/middleware_test.go b/sftpd/middleware_test.go index 4c8bc8ab..e285beba 100644 --- a/sftpd/middleware_test.go +++ b/sftpd/middleware_test.go @@ -165,14 +165,15 @@ func (Suite *PrefixMiddlewareSuite) TestFileListForwarding() { func (Suite *PrefixMiddlewareSuite) TestFileList() { var tests = []struct { - Method string - FilePath string - ExpectedErr error - ExpectedPath string + Method string + FilePath string + ExpectedErr error + ExpectedPath string + ExpectedItems int }{ - {Method: `List`, FilePath: `/random`, ExpectedErr: sftp.ErrSSHFxPermissionDenied}, - {Method: `List`, FilePath: `/`, ExpectedPath: `files`}, - {Method: `Stat`, FilePath: `/`, ExpectedPath: `/`}, + {Method: `List`, FilePath: `/random`, ExpectedErr: sftp.ErrSSHFxPermissionDenied, ExpectedItems: 0}, + {Method: `List`, FilePath: `/`, ExpectedPath: `files`, ExpectedItems: 2}, + {Method: `Stat`, FilePath: `/`, ExpectedPath: `/`, ExpectedItems: 1}, {Method: `NotAnOp`, ExpectedErr: sftp.ErrSSHFxOpUnsupported}, } @@ -189,10 +190,13 @@ func (Suite *PrefixMiddlewareSuite) TestFileList() { Suite.Nil(err) Suite.IsType(listerAt{}, ListerAt) if directList, ok := ListerAt.(listerAt); ok { - Suite.Len(directList, 1) - Suite.Equal(test.ExpectedPath, directList[0].Name()) - Suite.InDelta(time.Now().Unix(), directList[0].ModTime().Unix(), 1) - Suite.True(directList[0].IsDir()) + Suite.Len(directList, test.ExpectedItems) + if test.ExpectedItems > 1 { + Suite.Equal(".", directList[0].Name()) + } + Suite.Equal(test.ExpectedPath, directList[test.ExpectedItems-1].Name()) + Suite.InDelta(time.Now().Unix(), directList[test.ExpectedItems-1].ModTime().Unix(), 1) + Suite.True(directList[test.ExpectedItems-1].IsDir()) } } } diff --git a/sftpd/server.go b/sftpd/server.go index 4fceb048..cfe9728b 100644 --- a/sftpd/server.go +++ b/sftpd/server.go @@ -451,6 +451,7 @@ func (c *Configuration) AcceptInboundConnection(conn net.Conn, config *ssh.Serve RemoteAddr: conn.RemoteAddr(), LocalAddr: conn.LocalAddr(), channel: channel, + folderPrefix: c.FolderPrefix, } go c.handleSftpConnection(channel, &connection) } @@ -463,6 +464,7 @@ func (c *Configuration) AcceptInboundConnection(conn net.Conn, config *ssh.Serve RemoteAddr: conn.RemoteAddr(), LocalAddr: conn.LocalAddr(), channel: channel, + folderPrefix: c.FolderPrefix, } ok = processSSHCommand(req.Payload, &connection, c.EnabledSSHCommands) } diff --git a/util/util.go b/util/util.go index 6dfb2da1..55ccd950 100644 --- a/util/util.go +++ b/util/util.go @@ -608,3 +608,12 @@ func GetRedactedURL(rawurl string) string { } return u.Redacted() } + +// PrependFileInfo prepends a file info to a slice in an efficient way. +// We, optimistically, assume that the slice has enough capacity +func PrependFileInfo(files []os.FileInfo, info os.FileInfo) []os.FileInfo { + files = append(files, nil) + copy(files[1:], files) + files[0] = info + return files +}