mirror of
https://github.com/drakkan/sftpgo.git
synced 2025-12-07 23:00:55 +03:00
add dot and dot dot directories to sftp/ftp file listing
This commit is contained in:
@@ -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).
|
- `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.
|
- `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.
|
- `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
|
- **"ftpd"**, the configuration for the FTP server
|
||||||
- `bindings`, list of structs. Each struct has the following fields:
|
- `bindings`, list of structs. Each struct has the following fields:
|
||||||
- `port`, integer. The port used for serving FTP requests. 0 means disabled. Default: 0.
|
- `port`, integer. The port used for serving FTP requests. 0 means disabled. Default: 0.
|
||||||
|
|||||||
@@ -57,8 +57,9 @@ func TestBasicFTPHandlingCryptFs(t *testing.T) {
|
|||||||
}
|
}
|
||||||
list, err := client.List(".")
|
list, err := client.List(".")
|
||||||
if assert.NoError(t, err) {
|
if assert.NoError(t, err) {
|
||||||
assert.Len(t, list, 1)
|
assert.Len(t, list, 2)
|
||||||
assert.Equal(t, testFileSize, int64(list[0].Size))
|
assert.Equal(t, ".", list[0].Name)
|
||||||
|
assert.Equal(t, testFileSize, int64(list[1].Size))
|
||||||
}
|
}
|
||||||
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
|
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|||||||
@@ -536,6 +536,18 @@ func TestBasicFTPHandling(t *testing.T) {
|
|||||||
if assert.NoError(t, err) {
|
if assert.NoError(t, err) {
|
||||||
assert.Equal(t, path.Join("/", testDir), curDir)
|
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)
|
err = ftpUploadFile(testFilePath, testFileName, testFileSize, client, 0)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
size, err := client.FileSize(path.Join("/", testDir, testFileName))
|
size, err := client.FileSize(path.Join("/", testDir, testFileName))
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
"github.com/drakkan/sftpgo/v2/common"
|
"github.com/drakkan/sftpgo/v2/common"
|
||||||
"github.com/drakkan/sftpgo/v2/dataprovider"
|
"github.com/drakkan/sftpgo/v2/dataprovider"
|
||||||
"github.com/drakkan/sftpgo/v2/logger"
|
"github.com/drakkan/sftpgo/v2/logger"
|
||||||
|
"github.com/drakkan/sftpgo/v2/util"
|
||||||
"github.com/drakkan/sftpgo/v2/vfs"
|
"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) {
|
func (c *Connection) ReadDir(name string) ([]os.FileInfo, error) {
|
||||||
c.UpdateLastActivity()
|
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
|
// GetHandle implements ClientDriverExtentionFileTransfer
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
"github.com/drakkan/sftpgo/v2/common"
|
"github.com/drakkan/sftpgo/v2/common"
|
||||||
"github.com/drakkan/sftpgo/v2/dataprovider"
|
"github.com/drakkan/sftpgo/v2/dataprovider"
|
||||||
"github.com/drakkan/sftpgo/v2/logger"
|
"github.com/drakkan/sftpgo/v2/logger"
|
||||||
|
"github.com/drakkan/sftpgo/v2/util"
|
||||||
"github.com/drakkan/sftpgo/v2/vfs"
|
"github.com/drakkan/sftpgo/v2/vfs"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -21,10 +22,11 @@ type Connection struct {
|
|||||||
// client's version string
|
// client's version string
|
||||||
ClientVersion string
|
ClientVersion string
|
||||||
// Remote address for this connection
|
// Remote address for this connection
|
||||||
RemoteAddr net.Addr
|
RemoteAddr net.Addr
|
||||||
LocalAddr net.Addr
|
LocalAddr net.Addr
|
||||||
channel io.ReadWriteCloser
|
channel io.ReadWriteCloser
|
||||||
command string
|
command string
|
||||||
|
folderPrefix string
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetClientVersion returns the connected client's version
|
// GetClientVersion returns the connected client's version
|
||||||
@@ -201,6 +203,11 @@ func (c *Connection) Filelist(request *sftp.Request) (sftp.ListerAt, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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
|
return listerAt(files), nil
|
||||||
case "Stat":
|
case "Stat":
|
||||||
if !c.User.HasPerm(dataprovider.PermListItems, path.Dir(request.Filepath)) {
|
if !c.User.HasPerm(dataprovider.PermListItems, path.Dir(request.Filepath)) {
|
||||||
|
|||||||
@@ -78,10 +78,15 @@ func (p *prefixMiddleware) Filelist(request *sftp.Request) (sftp.ListerAt, error
|
|||||||
case pathIsPrefixParent:
|
case pathIsPrefixParent:
|
||||||
switch request.Method {
|
switch request.Method {
|
||||||
case methodList:
|
case methodList:
|
||||||
|
now := time.Now()
|
||||||
fileName := p.nextListFolder(request.Filepath)
|
fileName := p.nextListFolder(request.Filepath)
|
||||||
return listerAt([]os.FileInfo{
|
files := make([]os.FileInfo, 0, 3)
|
||||||
vfs.NewFileInfo(fileName, true, 0, time.Now(), false),
|
files = append(files, vfs.NewFileInfo(".", true, 0, now, false))
|
||||||
}), nil
|
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:
|
case methodStat:
|
||||||
return listerAt([]os.FileInfo{
|
return listerAt([]os.FileInfo{
|
||||||
vfs.NewFileInfo(request.Filepath, true, 0, time.Now(), false),
|
vfs.NewFileInfo(request.Filepath, true, 0, time.Now(), false),
|
||||||
|
|||||||
@@ -165,14 +165,15 @@ func (Suite *PrefixMiddlewareSuite) TestFileListForwarding() {
|
|||||||
|
|
||||||
func (Suite *PrefixMiddlewareSuite) TestFileList() {
|
func (Suite *PrefixMiddlewareSuite) TestFileList() {
|
||||||
var tests = []struct {
|
var tests = []struct {
|
||||||
Method string
|
Method string
|
||||||
FilePath string
|
FilePath string
|
||||||
ExpectedErr error
|
ExpectedErr error
|
||||||
ExpectedPath string
|
ExpectedPath string
|
||||||
|
ExpectedItems int
|
||||||
}{
|
}{
|
||||||
{Method: `List`, FilePath: `/random`, ExpectedErr: sftp.ErrSSHFxPermissionDenied},
|
{Method: `List`, FilePath: `/random`, ExpectedErr: sftp.ErrSSHFxPermissionDenied, ExpectedItems: 0},
|
||||||
{Method: `List`, FilePath: `/`, ExpectedPath: `files`},
|
{Method: `List`, FilePath: `/`, ExpectedPath: `files`, ExpectedItems: 2},
|
||||||
{Method: `Stat`, FilePath: `/`, ExpectedPath: `/`},
|
{Method: `Stat`, FilePath: `/`, ExpectedPath: `/`, ExpectedItems: 1},
|
||||||
{Method: `NotAnOp`, ExpectedErr: sftp.ErrSSHFxOpUnsupported},
|
{Method: `NotAnOp`, ExpectedErr: sftp.ErrSSHFxOpUnsupported},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,10 +190,13 @@ func (Suite *PrefixMiddlewareSuite) TestFileList() {
|
|||||||
Suite.Nil(err)
|
Suite.Nil(err)
|
||||||
Suite.IsType(listerAt{}, ListerAt)
|
Suite.IsType(listerAt{}, ListerAt)
|
||||||
if directList, ok := ListerAt.(listerAt); ok {
|
if directList, ok := ListerAt.(listerAt); ok {
|
||||||
Suite.Len(directList, 1)
|
Suite.Len(directList, test.ExpectedItems)
|
||||||
Suite.Equal(test.ExpectedPath, directList[0].Name())
|
if test.ExpectedItems > 1 {
|
||||||
Suite.InDelta(time.Now().Unix(), directList[0].ModTime().Unix(), 1)
|
Suite.Equal(".", directList[0].Name())
|
||||||
Suite.True(directList[0].IsDir())
|
}
|
||||||
|
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())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -451,6 +451,7 @@ func (c *Configuration) AcceptInboundConnection(conn net.Conn, config *ssh.Serve
|
|||||||
RemoteAddr: conn.RemoteAddr(),
|
RemoteAddr: conn.RemoteAddr(),
|
||||||
LocalAddr: conn.LocalAddr(),
|
LocalAddr: conn.LocalAddr(),
|
||||||
channel: channel,
|
channel: channel,
|
||||||
|
folderPrefix: c.FolderPrefix,
|
||||||
}
|
}
|
||||||
go c.handleSftpConnection(channel, &connection)
|
go c.handleSftpConnection(channel, &connection)
|
||||||
}
|
}
|
||||||
@@ -463,6 +464,7 @@ func (c *Configuration) AcceptInboundConnection(conn net.Conn, config *ssh.Serve
|
|||||||
RemoteAddr: conn.RemoteAddr(),
|
RemoteAddr: conn.RemoteAddr(),
|
||||||
LocalAddr: conn.LocalAddr(),
|
LocalAddr: conn.LocalAddr(),
|
||||||
channel: channel,
|
channel: channel,
|
||||||
|
folderPrefix: c.FolderPrefix,
|
||||||
}
|
}
|
||||||
ok = processSSHCommand(req.Payload, &connection, c.EnabledSSHCommands)
|
ok = processSSHCommand(req.Payload, &connection, c.EnabledSSHCommands)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -608,3 +608,12 @@ func GetRedactedURL(rawurl string) string {
|
|||||||
}
|
}
|
||||||
return u.Redacted()
|
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
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user