mirror of
https://github.com/drakkan/sftpgo.git
synced 2026-03-14 14:25:52 +01:00
Normalize FTP remove paths
This commit is contained in:
parent
133d2692c4
commit
498a33930d
3 changed files with 138 additions and 24 deletions
2
go.mod
2
go.mod
|
|
@ -67,6 +67,7 @@ require (
|
|||
golang.org/x/oauth2 v0.34.0
|
||||
golang.org/x/sys v0.39.0
|
||||
golang.org/x/term v0.38.0
|
||||
golang.org/x/text v0.32.0
|
||||
golang.org/x/time v0.14.0
|
||||
google.golang.org/api v0.257.0
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||
|
|
@ -169,7 +170,6 @@ require (
|
|||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/mod v0.31.0 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/text v0.32.0 // indirect
|
||||
golang.org/x/tools v0.40.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
|
||||
google.golang.org/genproto v0.0.0-20251222181119-0a764e51fe1b // indirect
|
||||
|
|
|
|||
|
|
@ -22,9 +22,11 @@ import (
|
|||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
ftpserver "github.com/fclairamb/ftpserverlib"
|
||||
"github.com/spf13/afero"
|
||||
"golang.org/x/text/encoding/charmap"
|
||||
|
||||
"github.com/drakkan/sftpgo/v2/internal/common"
|
||||
"github.com/drakkan/sftpgo/v2/internal/dataprovider"
|
||||
|
|
@ -98,7 +100,12 @@ func (c *Connection) Create(_ string) (afero.File, error) {
|
|||
func (c *Connection) Mkdir(name string, _ os.FileMode) error {
|
||||
c.UpdateLastActivity()
|
||||
|
||||
return c.CreateDir(name, true)
|
||||
normalizedName, err := c.normalizeFTPPath(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.CreateDir(normalizedName, true)
|
||||
}
|
||||
|
||||
// MkdirAll is not implemented, we don't need it
|
||||
|
|
@ -121,7 +128,12 @@ func (c *Connection) OpenFile(_ string, _ int, _ os.FileMode) (afero.File, error
|
|||
func (c *Connection) Remove(name string) error {
|
||||
c.UpdateLastActivity()
|
||||
|
||||
fs, p, err := c.GetFsAndResolvedPath(name)
|
||||
normalizedName, err := c.normalizeFTPPath(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fs, p, err := c.GetFsAndResolvedPath(normalizedName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -136,7 +148,7 @@ func (c *Connection) Remove(name string) error {
|
|||
c.Log(logger.LevelError, "cannot remove %q is not a file/symlink", p)
|
||||
return c.GetGenericError(nil)
|
||||
}
|
||||
return c.RemoveFile(fs, p, name, fi)
|
||||
return c.RemoveFile(fs, p, normalizedName, fi)
|
||||
}
|
||||
|
||||
// RemoveAll is not implemented, we don't need it
|
||||
|
|
@ -148,7 +160,16 @@ func (c *Connection) RemoveAll(_ string) error {
|
|||
func (c *Connection) Rename(oldname, newname string) error {
|
||||
c.UpdateLastActivity()
|
||||
|
||||
return c.BaseConnection.Rename(oldname, newname)
|
||||
normalizedOldname, err := c.normalizeFTPPath(oldname)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
normalizedNewname, err := c.normalizeFTPPath(newname)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.BaseConnection.Rename(normalizedOldname, normalizedNewname)
|
||||
}
|
||||
|
||||
// Stat returns a FileInfo describing the named file/directory, or an error,
|
||||
|
|
@ -157,15 +178,20 @@ func (c *Connection) Stat(name string) (os.FileInfo, error) {
|
|||
c.UpdateLastActivity()
|
||||
c.doWildcardListDir = false
|
||||
|
||||
if !c.User.HasPerm(dataprovider.PermListItems, path.Dir(name)) {
|
||||
normalizedName, err := c.normalizeFTPPath(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !c.User.HasPerm(dataprovider.PermListItems, path.Dir(normalizedName)) {
|
||||
return nil, c.GetPermissionDeniedError()
|
||||
}
|
||||
|
||||
fi, err := c.DoStat(name, 0, true)
|
||||
fi, err := c.DoStat(normalizedName, 0, true)
|
||||
if err != nil {
|
||||
if c.isListDirWithWildcards(path.Base(name)) {
|
||||
if c.isListDirWithWildcards(path.Base(normalizedName)) {
|
||||
c.doWildcardListDir = true
|
||||
return vfs.NewFileInfo(name, true, 0, time.Unix(0, 0), false), nil
|
||||
return vfs.NewFileInfo(normalizedName, true, 0, time.Unix(0, 0), false), nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -199,30 +225,45 @@ func (c *Connection) Chown(_ string, _, _ int) error {
|
|||
func (c *Connection) Chmod(name string, mode os.FileMode) error {
|
||||
c.UpdateLastActivity()
|
||||
|
||||
normalizedName, err := c.normalizeFTPPath(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
attrs := common.StatAttributes{
|
||||
Flags: common.StatAttrPerms,
|
||||
Mode: mode,
|
||||
}
|
||||
return c.SetStat(name, &attrs)
|
||||
return c.SetStat(normalizedName, &attrs)
|
||||
}
|
||||
|
||||
// Chtimes changes the access and modification times of the named file
|
||||
func (c *Connection) Chtimes(name string, atime time.Time, mtime time.Time) error {
|
||||
c.UpdateLastActivity()
|
||||
|
||||
normalizedName, err := c.normalizeFTPPath(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
attrs := common.StatAttributes{
|
||||
Flags: common.StatAttrTimes,
|
||||
Atime: atime,
|
||||
Mtime: mtime,
|
||||
}
|
||||
return c.SetStat(name, &attrs)
|
||||
return c.SetStat(normalizedName, &attrs)
|
||||
}
|
||||
|
||||
// GetAvailableSpace implements ClientDriverExtensionAvailableSpace interface
|
||||
func (c *Connection) GetAvailableSpace(dirName string) (int64, error) {
|
||||
c.UpdateLastActivity()
|
||||
|
||||
diskQuota, transferQuota := c.HasSpace(false, false, path.Join(dirName, "fakefile.txt"))
|
||||
normalizedDirName, err := c.normalizeFTPPath(dirName)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
diskQuota, transferQuota := c.HasSpace(false, false, path.Join(normalizedDirName, "fakefile.txt"))
|
||||
if !diskQuota.HasSpace || !transferQuota.HasUploadSpace() {
|
||||
return 0, nil
|
||||
}
|
||||
|
|
@ -233,7 +274,7 @@ func (c *Connection) GetAvailableSpace(dirName string) (int64, error) {
|
|||
return c.User.Filters.MaxUploadFileSize, nil
|
||||
}
|
||||
|
||||
fs, p, err := c.GetFsAndResolvedPath(dirName)
|
||||
fs, p, err := c.GetFsAndResolvedPath(normalizedDirName)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
|
@ -280,29 +321,48 @@ func (c *Connection) AllocateSpace(_ int) error {
|
|||
func (c *Connection) RemoveDir(name string) error {
|
||||
c.UpdateLastActivity()
|
||||
|
||||
return c.BaseConnection.RemoveDir(name)
|
||||
normalizedName, err := c.normalizeFTPPath(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.BaseConnection.RemoveDir(normalizedName)
|
||||
}
|
||||
|
||||
// Symlink implements ClientDriverExtensionSymlink
|
||||
func (c *Connection) Symlink(oldname, newname string) error {
|
||||
c.UpdateLastActivity()
|
||||
|
||||
return c.CreateSymlink(oldname, newname)
|
||||
normalizedOldname, err := c.normalizeFTPPath(oldname)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
normalizedNewname, err := c.normalizeFTPPath(newname)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.CreateSymlink(normalizedOldname, normalizedNewname)
|
||||
}
|
||||
|
||||
// ReadDir implements ClientDriverExtensionFilelist
|
||||
func (c *Connection) ReadDir(name string) ([]os.FileInfo, error) {
|
||||
c.UpdateLastActivity()
|
||||
|
||||
normalizedName, err := c.normalizeFTPPath(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if c.doWildcardListDir {
|
||||
c.doWildcardListDir = false
|
||||
baseName := path.Base(name)
|
||||
baseName := path.Base(normalizedName)
|
||||
// we only support wildcards for the last path level, for example:
|
||||
// - *.xml is supported
|
||||
// - dir*/*.xml is not supported
|
||||
name = path.Dir(name)
|
||||
c.clientContext.SetListPath(name)
|
||||
lister, err := c.ListDir(name)
|
||||
normalizedName = path.Dir(normalizedName)
|
||||
c.clientContext.SetListPath(normalizedName)
|
||||
lister, err := c.ListDir(normalizedName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -310,13 +370,13 @@ func (c *Connection) ReadDir(name string) ([]os.FileInfo, error) {
|
|||
DirLister: lister,
|
||||
pattern: baseName,
|
||||
lastCommand: c.clientContext.GetLastCommand(),
|
||||
dirName: name,
|
||||
dirName: normalizedName,
|
||||
connectionPath: c.clientContext.Path(),
|
||||
}
|
||||
return consumeDirLister(patternLister)
|
||||
}
|
||||
|
||||
lister, err := c.ListDir(name)
|
||||
lister, err := c.ListDir(normalizedName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -327,7 +387,12 @@ func (c *Connection) ReadDir(name string) ([]os.FileInfo, error) {
|
|||
func (c *Connection) GetHandle(name string, flags int, offset int64) (ftpserver.FileTransfer, error) {
|
||||
c.UpdateLastActivity()
|
||||
|
||||
fs, p, err := c.GetFsAndResolvedPath(name)
|
||||
normalizedName, err := c.normalizeFTPPath(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fs, p, err := c.GetFsAndResolvedPath(normalizedName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -342,9 +407,9 @@ func (c *Connection) GetHandle(name string, flags int, offset int64) (ftpserver.
|
|||
}
|
||||
|
||||
if flags&os.O_WRONLY != 0 {
|
||||
return c.uploadFile(fs, p, name, flags)
|
||||
return c.uploadFile(fs, p, normalizedName, flags)
|
||||
}
|
||||
return c.downloadFile(fs, p, name, offset)
|
||||
return c.downloadFile(fs, p, normalizedName, offset)
|
||||
}
|
||||
|
||||
func (c *Connection) downloadFile(fs vfs.Fs, fsPath, ftpPath string, offset int64) (ftpserver.FileTransfer, error) {
|
||||
|
|
@ -531,6 +596,19 @@ func (c *Connection) isListDirWithWildcards(name string) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
func (c *Connection) normalizeFTPPath(name string) (string, error) {
|
||||
if name == "" || utf8.ValidString(name) {
|
||||
return name, nil
|
||||
}
|
||||
|
||||
decoded, err := charmap.ISO8859_1.NewDecoder().String(name)
|
||||
if err != nil {
|
||||
c.Log(logger.LevelError, "error decoding FTP path %q: %v", name, err)
|
||||
return "", ftpserver.ErrFileNameNotAllowed
|
||||
}
|
||||
return decoded, nil
|
||||
}
|
||||
|
||||
func getPathRelativeTo(base, target string) string {
|
||||
var sb strings.Builder
|
||||
for {
|
||||
|
|
|
|||
36
internal/ftpd/handler_test.go
Normal file
36
internal/ftpd/handler_test.go
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
package ftpd
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/drakkan/sftpgo/v2/internal/common"
|
||||
"github.com/drakkan/sftpgo/v2/internal/dataprovider"
|
||||
)
|
||||
|
||||
func TestNormalizeFTPPathLatin1(t *testing.T) {
|
||||
conn := &Connection{
|
||||
BaseConnection: common.NewBaseConnection("test-conn", common.ProtocolFTP, "127.0.0.1", "127.0.0.1",
|
||||
dataprovider.User{}),
|
||||
}
|
||||
latin1 := string([]byte{0x66, 0x6f, 0x6f, 0xe9, 0x2e, 0x74, 0x78, 0x74})
|
||||
|
||||
normalized, err := conn.normalizeFTPPath(latin1)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "fooé.txt", normalized)
|
||||
}
|
||||
|
||||
func TestNormalizeFTPPathUTF8(t *testing.T) {
|
||||
conn := &Connection{
|
||||
BaseConnection: common.NewBaseConnection("test-conn", common.ProtocolFTP, "127.0.0.1", "127.0.0.1",
|
||||
dataprovider.User{}),
|
||||
}
|
||||
utf8Name := "déjà-vu.txt"
|
||||
|
||||
normalized, err := conn.normalizeFTPPath(utf8Name)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, utf8Name, normalized)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue