mirror of
https://github.com/drakkan/sftpgo.git
synced 2025-12-08 07:10:56 +03:00
sftpd: minor improvements and docs for the prefix middleware
This commit is contained in:
@@ -2061,3 +2061,26 @@ func newFakeListener(err error) net.Listener {
|
||||
err: err,
|
||||
}
|
||||
}
|
||||
|
||||
func TestFolderPrefix(t *testing.T) {
|
||||
c := Configuration{
|
||||
FolderPrefix: "files",
|
||||
}
|
||||
c.checkFolderPrefix()
|
||||
assert.Equal(t, "/files", c.FolderPrefix)
|
||||
c.FolderPrefix = ""
|
||||
c.checkFolderPrefix()
|
||||
assert.Empty(t, c.FolderPrefix)
|
||||
c.FolderPrefix = "/"
|
||||
c.checkFolderPrefix()
|
||||
assert.Empty(t, c.FolderPrefix)
|
||||
c.FolderPrefix = "/."
|
||||
c.checkFolderPrefix()
|
||||
assert.Empty(t, c.FolderPrefix)
|
||||
c.FolderPrefix = "."
|
||||
c.checkFolderPrefix()
|
||||
assert.Empty(t, c.FolderPrefix)
|
||||
c.FolderPrefix = ".."
|
||||
c.checkFolderPrefix()
|
||||
assert.Empty(t, c.FolderPrefix)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -13,7 +12,7 @@ import (
|
||||
"github.com/drakkan/sftpgo/v2/vfs"
|
||||
)
|
||||
|
||||
// Middleware defines the interface for sftp middlewares
|
||||
// Middleware defines the interface for SFTP middlewares
|
||||
type Middleware interface {
|
||||
sftp.FileReader
|
||||
sftp.FileWriter
|
||||
@@ -77,17 +76,15 @@ func (p *prefixMiddleware) Filelist(request *sftp.Request) (sftp.ListerAt, error
|
||||
request.Filepath, _ = p.removeFolderPrefix(request.Filepath)
|
||||
return p.next.Filelist(request)
|
||||
case pathIsPrefixParent:
|
||||
Now := time.Now()
|
||||
switch request.Method {
|
||||
case methodList:
|
||||
FileName := p.nextListFolder(request.Filepath)
|
||||
fileName := p.nextListFolder(request.Filepath)
|
||||
return listerAt([]os.FileInfo{
|
||||
// vfs.NewFileInfo(`.`, true, 0, Now, false),
|
||||
vfs.NewFileInfo(FileName, true, 0, Now, false),
|
||||
vfs.NewFileInfo(fileName, true, 0, time.Now(), false),
|
||||
}), nil
|
||||
case methodStat:
|
||||
return listerAt([]os.FileInfo{
|
||||
vfs.NewFileInfo(request.Filepath, true, 0, Now, false),
|
||||
vfs.NewFileInfo(request.Filepath, true, 0, time.Now(), false),
|
||||
}), nil
|
||||
default:
|
||||
return nil, sftp.ErrSSHFxOpUnsupported
|
||||
@@ -153,16 +150,16 @@ func (p *prefixMiddleware) StatVFS(request *sftp.Request) (*sftp.StatVFS, error)
|
||||
}
|
||||
|
||||
func (p *prefixMiddleware) nextListFolder(requestPath string) string {
|
||||
cleanPath := filepath.Clean(`/` + requestPath)
|
||||
cleanPrefix := filepath.Clean(`/` + p.prefix)
|
||||
cleanPath := path.Clean(`/` + requestPath)
|
||||
cleanPrefix := path.Clean(`/` + p.prefix)
|
||||
|
||||
FileName := cleanPrefix[len(cleanPath):]
|
||||
FileName = strings.TrimLeft(FileName, `/`)
|
||||
SlashIndex := strings.Index(FileName, `/`)
|
||||
if SlashIndex > 0 {
|
||||
return FileName[0:SlashIndex]
|
||||
fileName := cleanPrefix[len(cleanPath):]
|
||||
fileName = strings.TrimLeft(fileName, `/`)
|
||||
slashIndex := strings.Index(fileName, `/`)
|
||||
if slashIndex > 0 {
|
||||
return fileName[0:slashIndex]
|
||||
}
|
||||
return FileName
|
||||
return fileName
|
||||
}
|
||||
|
||||
func (p *prefixMiddleware) containsPrefix(virtualPath string) bool {
|
||||
@@ -184,7 +181,7 @@ func (p *prefixMiddleware) removeFolderPrefix(virtualPath string) (string, bool)
|
||||
return virtualPath, true
|
||||
}
|
||||
|
||||
virtualPath = filepath.Clean(`/` + virtualPath)
|
||||
virtualPath = path.Clean(`/` + virtualPath)
|
||||
if p.containsPrefix(virtualPath) {
|
||||
effectivePath := virtualPath[len(p.prefix):]
|
||||
if effectivePath == `` {
|
||||
@@ -195,9 +192,9 @@ func (p *prefixMiddleware) removeFolderPrefix(virtualPath string) (string, bool)
|
||||
return virtualPath, false
|
||||
}
|
||||
|
||||
func getPrefixHierarchy(prefix, path string) prefixMatch {
|
||||
prefixSplit := strings.Split(filepath.Clean(`/`+prefix), `/`)
|
||||
pathSplit := strings.Split(filepath.Clean(`/`+path), `/`)
|
||||
func getPrefixHierarchy(prefix, virtualPath string) prefixMatch {
|
||||
prefixSplit := strings.Split(path.Clean(`/`+prefix), `/`)
|
||||
pathSplit := strings.Split(path.Clean(`/`+virtualPath), `/`)
|
||||
|
||||
for {
|
||||
// stop if either slice is empty of the current head elements do not match
|
||||
|
||||
@@ -109,6 +109,31 @@ func (Suite *PrefixMiddlewareSuite) TestOpenFile() {
|
||||
}
|
||||
}
|
||||
|
||||
func (Suite *PrefixMiddlewareSuite) TestStatVFS() {
|
||||
prefix := prefixMiddleware{prefix: `/files`}
|
||||
|
||||
// parent of prefix
|
||||
res, err := prefix.StatVFS(&sftp.Request{Filepath: `/`})
|
||||
Suite.Nil(res)
|
||||
Suite.Equal(sftp.ErrSSHFxPermissionDenied, err)
|
||||
|
||||
// file path and prefix are unrelated
|
||||
res, err = prefix.StatVFS(&sftp.Request{Filepath: `/random`})
|
||||
Suite.Nil(res)
|
||||
Suite.Equal(sftp.ErrSSHFxPermissionDenied, err)
|
||||
|
||||
// file path is sub path of configured prefix
|
||||
// mocked returns are not import, just the call to the next file writer
|
||||
statVFSMock := mocks.NewMockMiddleware(Suite.MockCtl)
|
||||
statVFSMock.EXPECT().
|
||||
StatVFS(&sftp.Request{Filepath: `/data`}).
|
||||
Return(nil, nil)
|
||||
prefix.next = statVFSMock
|
||||
res, err = prefix.StatVFS(&sftp.Request{Filepath: `/files/data`})
|
||||
Suite.Nil(err)
|
||||
Suite.Nil(res)
|
||||
}
|
||||
|
||||
func (Suite *PrefixMiddlewareSuite) TestFileListForwarding() {
|
||||
var tests = []struct {
|
||||
Method string
|
||||
|
||||
@@ -119,7 +119,11 @@ type Configuration struct {
|
||||
KeyboardInteractiveHook string `json:"keyboard_interactive_auth_hook" mapstructure:"keyboard_interactive_auth_hook"`
|
||||
// PasswordAuthentication specifies whether password authentication is allowed.
|
||||
PasswordAuthentication bool `json:"password_authentication" mapstructure:"password_authentication"`
|
||||
// Virtual root folder prefix to include in all file operations (ex: /files)
|
||||
// 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 migrations from OpenSSH. It is not recommended for general usage.
|
||||
FolderPrefix string `json:"folder_prefix" mapstructure:"folder_prefix"`
|
||||
certChecker *ssh.CertChecker
|
||||
parsedUserCAKeys []ssh.PublicKey
|
||||
@@ -479,27 +483,8 @@ func (c *Configuration) handleSftpConnection(channel ssh.Channel, connection *Co
|
||||
common.Connections.Add(connection)
|
||||
defer common.Connections.Remove(connection.GetID())
|
||||
|
||||
var handlers sftp.Handlers
|
||||
|
||||
if c.FolderPrefix != "" {
|
||||
prefixMiddleware := newPrefixMiddleware(c.FolderPrefix, connection)
|
||||
handlers = sftp.Handlers{
|
||||
FileGet: prefixMiddleware,
|
||||
FilePut: prefixMiddleware,
|
||||
FileCmd: prefixMiddleware,
|
||||
FileList: prefixMiddleware,
|
||||
}
|
||||
} else {
|
||||
handlers = sftp.Handlers{
|
||||
FileGet: connection,
|
||||
FilePut: connection,
|
||||
FileCmd: connection,
|
||||
FileList: connection,
|
||||
}
|
||||
}
|
||||
|
||||
// Create the server instance for the channel using the handler we created above.
|
||||
server := sftp.NewRequestServer(channel, handlers, sftp.WithRSAllocator())
|
||||
server := sftp.NewRequestServer(channel, c.createHandlers(connection), sftp.WithRSAllocator())
|
||||
|
||||
defer server.Close()
|
||||
if err := server.Serve(); err == io.EOF {
|
||||
@@ -512,6 +497,26 @@ func (c *Configuration) handleSftpConnection(channel ssh.Channel, connection *Co
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Configuration) createHandlers(connection *Connection) sftp.Handlers {
|
||||
if c.FolderPrefix != "" {
|
||||
prefixMiddleware := newPrefixMiddleware(c.FolderPrefix, connection)
|
||||
|
||||
return sftp.Handlers{
|
||||
FileGet: prefixMiddleware,
|
||||
FilePut: prefixMiddleware,
|
||||
FileCmd: prefixMiddleware,
|
||||
FileList: prefixMiddleware,
|
||||
}
|
||||
}
|
||||
|
||||
return sftp.Handlers{
|
||||
FileGet: connection,
|
||||
FilePut: connection,
|
||||
FileCmd: connection,
|
||||
FileList: connection,
|
||||
}
|
||||
}
|
||||
|
||||
func checkAuthError(ip string, err error) {
|
||||
if authErrors, ok := err.(*ssh.ServerAuthError); ok {
|
||||
// check public key auth errors here
|
||||
@@ -604,7 +609,13 @@ func (c *Configuration) checkSSHCommands() {
|
||||
func (c *Configuration) checkFolderPrefix() {
|
||||
if c.FolderPrefix != "" {
|
||||
c.FolderPrefix = path.Join("/", c.FolderPrefix)
|
||||
logger.Debug(logSender, "", "folder prefix %#v configured", c.FolderPrefix)
|
||||
if c.FolderPrefix == "/" {
|
||||
c.FolderPrefix = ""
|
||||
}
|
||||
}
|
||||
if c.FolderPrefix != "" {
|
||||
c.EnabledSSHCommands = nil
|
||||
logger.Debug(logSender, "", "folder prefix %#v configured, SSH commands are disabled", c.FolderPrefix)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -277,6 +277,26 @@ func TestMain(m *testing.M) {
|
||||
waitTCPListening(sftpdConf.Bindings[0].GetAddress())
|
||||
getHostKeysFingerprints(sftpdConf.HostKeys)
|
||||
|
||||
prefixedConf := sftpdConf
|
||||
prefixedConf.Bindings = []sftpd.Binding{
|
||||
{
|
||||
Port: 2226,
|
||||
ApplyProxyConfig: false,
|
||||
},
|
||||
}
|
||||
prefixedConf.PasswordAuthentication = true
|
||||
prefixedConf.FolderPrefix = "/prefix/files"
|
||||
go func() {
|
||||
logger.Debug(logSender, "", "initializing SFTP server with config %+v and proxy protocol %v",
|
||||
prefixedConf, common.Config.ProxyProtocol)
|
||||
if err := prefixedConf.Initialize(configDir); err != nil {
|
||||
logger.ErrorToConsole("could not start SFTP server with proxy protocol 2: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}()
|
||||
|
||||
waitTCPListening(prefixedConf.Bindings[0].GetAddress())
|
||||
|
||||
exitCode := m.Run()
|
||||
os.Remove(logFilePath)
|
||||
os.Remove(loginBannerFile)
|
||||
@@ -481,6 +501,56 @@ func TestBasicSFTPFsHandling(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestFolderPrefix(t *testing.T) {
|
||||
usePubKey := true
|
||||
u := getTestUser(usePubKey)
|
||||
u.QuotaFiles = 1000
|
||||
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
conn, client, err := getSftpClientWithAddr(user, usePubKey, "127.0.0.1:2226")
|
||||
if assert.NoError(t, err) {
|
||||
defer conn.Close()
|
||||
defer client.Close()
|
||||
err = checkBasicSFTP(client)
|
||||
assert.NoError(t, err)
|
||||
_, err = client.Stat("path")
|
||||
assert.ErrorIs(t, err, os.ErrPermission)
|
||||
_, err = client.Stat("/prefix/path")
|
||||
assert.ErrorIs(t, err, os.ErrPermission)
|
||||
_, err = client.Stat("/prefix/files1")
|
||||
assert.ErrorIs(t, err, os.ErrPermission)
|
||||
contents, err := client.ReadDir("/")
|
||||
if assert.NoError(t, err) {
|
||||
if assert.Len(t, contents, 1) {
|
||||
assert.Equal(t, "prefix", contents[0].Name())
|
||||
}
|
||||
}
|
||||
contents, err = client.ReadDir("/prefix")
|
||||
if assert.NoError(t, err) {
|
||||
if assert.Len(t, contents, 1) {
|
||||
assert.Equal(t, "files", contents[0].Name())
|
||||
}
|
||||
}
|
||||
_, err = client.OpenFile(testFileName, os.O_WRONLY)
|
||||
assert.ErrorIs(t, err, os.ErrPermission)
|
||||
_, err = client.OpenFile(testFileName, os.O_RDONLY)
|
||||
assert.ErrorIs(t, err, os.ErrPermission)
|
||||
|
||||
f, err := client.OpenFile(path.Join("prefix", "files", testFileName), os.O_WRONLY)
|
||||
assert.NoError(t, err)
|
||||
_, err = f.Write([]byte("test"))
|
||||
assert.NoError(t, err)
|
||||
err = f.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 TestLoginNonExistentUser(t *testing.T) {
|
||||
usePubKey := true
|
||||
user := getTestUser(usePubKey)
|
||||
|
||||
Reference in New Issue
Block a user