remove rsync support

rsync was executed as an external command, which means we have no insight
into or control over what it actually does.
From a security perspective, this is far from ideal.

To be clear, there's nothing inherently wrong with rsync itself. However,
if we were to support it properly within SFTPGo, we would need to implement
the low-level protocol internally rather than relying on launching an external
process. This would ensure it works seamlessly with any storage backend,
just as SFTP does, for example.
We recommend using one of the many alternatives that rely on the SFTP
protocol, such as rclone

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
Nicola Murino
2025-09-28 18:15:15 +02:00
parent cc0ee9f43b
commit 35525e22e9
13 changed files with 31 additions and 1035 deletions

View File

@@ -1412,19 +1412,6 @@ func TestUserPerms(t *testing.T) {
u.Permissions["/"] = []string{dataprovider.PermDeleteDirs, dataprovider.PermRenameFiles, dataprovider.PermRenameDirs} u.Permissions["/"] = []string{dataprovider.PermDeleteDirs, dataprovider.PermRenameFiles, dataprovider.PermRenameDirs}
assert.False(t, u.HasPermsDeleteAll("/")) assert.False(t, u.HasPermsDeleteAll("/"))
assert.True(t, u.HasPermsRenameAll("/")) assert.True(t, u.HasPermsRenameAll("/"))
toCheck := []string{dataprovider.PermDownload, dataprovider.PermUpload, dataprovider.PermCreateDirs, dataprovider.PermListItems,
dataprovider.PermOverwrite, dataprovider.PermDelete}
u.Permissions = make(map[string][]string)
u.Permissions["/"] = []string{dataprovider.PermListItems}
u.Permissions["/example-dir/bar"] = []string{dataprovider.PermListItems}
u.Permissions["/example-dir"] = toCheck
assert.True(t, u.HasPerms(toCheck, "/example-dir"))
assert.False(t, u.HasRecursivePerms(toCheck, "/example-dir"))
delete(u.Permissions, "/example-dir/bar")
assert.True(t, u.HasRecursivePerms(toCheck, "/example-dir"))
u.Permissions["/example-dirbar"] = []string{dataprovider.PermListItems}
assert.True(t, u.HasRecursivePerms(toCheck, "/example-dir"))
} }
func TestGetTLSVersion(t *testing.T) { func TestGetTLSVersion(t *testing.T) {

View File

@@ -876,27 +876,6 @@ func (u *User) HasAnyPerm(permissions []string, path string) bool {
return false return false
} }
// HasRecursivePerms returns true if the user has all the specified permissions
// in the given folder and in every subfolder that has explicit permissions
// defined.
func (u *User) HasRecursivePerms(permissions []string, virtualPath string) bool {
if !u.HasPerms(permissions, virtualPath) {
return false
}
for dir, perms := range u.Permissions {
if len(dir) > len(virtualPath) {
if strings.HasPrefix(dir, virtualPath+"/") {
for _, permission := range permissions {
if !slices.Contains(perms, permission) {
return false
}
}
}
}
}
return true
}
// HasPerms returns true if the user has all the given permissions // HasPerms returns true if the user has all the given permissions
func (u *User) HasPerms(permissions []string, path string) bool { func (u *User) HasPerms(permissions []string, path string) bool {
perms := u.GetPermissionsForPath(path) perms := u.GetPermissionsForPath(path)

View File

@@ -1,37 +0,0 @@
// Copyright (C) 2019 Nicola Murino
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published
// by the Free Software Foundation, version 3.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//go:build !windows
package sftpd
import (
"os"
"os/exec"
"syscall"
)
var (
processUID = os.Geteuid()
processGID = os.Getegid()
)
func wrapCmd(cmd *exec.Cmd, uid, gid int) *exec.Cmd {
isCurrentUser := processUID == uid && processGID == gid
if (uid > 0 || gid > 0) && !isCurrentUser {
cmd.SysProcAttr = &syscall.SysProcAttr{}
cmd.SysProcAttr.Credential = &syscall.Credential{Uid: uint32(uid), Gid: uint32(gid)}
}
return cmd
}

View File

@@ -1,23 +0,0 @@
// Copyright (C) 2019 Nicola Murino
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published
// by the Free Software Foundation, version 3.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package sftpd
import (
"os/exec"
)
func wrapCmd(cmd *exec.Cmd, _, _ int) *exec.Cmd {
return cmd
}

View File

@@ -538,33 +538,6 @@ func TestSSHCommandErrors(t *testing.T) {
_, err = cmd.connection.User.GetFilesystem("123") _, err = cmd.connection.User.GetFilesystem("123")
assert.NoError(t, err) assert.NoError(t, err)
cmd.command = "git-receive-pack"
command, err := cmd.getSystemCommand()
assert.NoError(t, err)
err = cmd.executeSystemCommand(command)
assert.Error(t, err, "invalid command must fail")
command, err = cmd.getSystemCommand()
assert.NoError(t, err)
_, err = command.cmd.StderrPipe()
assert.NoError(t, err)
err = cmd.executeSystemCommand(command)
assert.Error(t, err, "command must fail, pipe was already assigned")
err = cmd.executeSystemCommand(command)
assert.Error(t, err, "command must fail, pipe was already assigned")
command, err = cmd.getSystemCommand()
assert.NoError(t, err)
_, err = command.cmd.StdoutPipe()
assert.NoError(t, err)
err = cmd.executeSystemCommand(command)
assert.Error(t, err, "command must fail, pipe was already assigned")
cmd = sshCommand{ cmd = sshCommand{
command: "sftpgo-remove", command: "sftpgo-remove",
connection: &connection, connection: &connection,
@@ -581,23 +554,6 @@ func TestSSHCommandErrors(t *testing.T) {
err = cmd.handle() err = cmd.handle()
assert.Error(t, err, "ssh command must fail, we are requesting an invalid path") assert.Error(t, err, "ssh command must fail, we are requesting an invalid path")
cmd.connection.User.HomeDir = filepath.Clean(os.TempDir())
cmd = sshCommand{
command: "sftpgo-copy",
connection: &connection,
args: []string{"src", "dst"},
}
cmd.connection.User.Permissions = make(map[string][]string)
cmd.connection.User.Permissions["/"] = []string{dataprovider.PermAny}
common.WaitForTransfers(1)
_, err = cmd.getSystemCommand()
if assert.Error(t, err) {
assert.Contains(t, err.Error(), common.ErrShuttingDown.Error())
}
err = common.Initialize(common.Config, 0) err = common.Initialize(common.Config, 0)
assert.NoError(t, err) assert.NoError(t, err)
} }
@@ -638,14 +594,6 @@ func TestCommandsWithExtensionsFilter(t *testing.T) {
} }
err := cmd.handleHashCommands() err := cmd.handleHashCommands()
assert.EqualError(t, err, common.ErrPermissionDenied.Error()) assert.EqualError(t, err, common.ErrPermissionDenied.Error())
cmd = sshCommand{
command: "rsync",
connection: connection,
args: []string{"--server", "-vlogDtprze.iLsfxC", ".", "/"},
}
_, err = cmd.getSystemCommand()
assert.EqualError(t, err, errUnsupportedConfig.Error())
} }
func TestSSHCommandsRemoteFs(t *testing.T) { func TestSSHCommandsRemoteFs(t *testing.T) {
@@ -676,17 +624,7 @@ func TestSSHCommandsRemoteFs(t *testing.T) {
args: []string{}, args: []string{},
} }
command, err := cmd.getSystemCommand() err := cmd.handleSFTPGoCopy()
assert.NoError(t, err)
err = cmd.executeSystemCommand(command)
assert.Error(t, err, "command must fail for a non local filesystem")
cmd = sshCommand{
command: "sftpgo-copy",
connection: connection,
args: []string{},
}
err = cmd.handleSFTPGoCopy()
assert.Error(t, err) assert.Error(t, err)
cmd = sshCommand{ cmd = sshCommand{
command: "sftpgo-remove", command: "sftpgo-remove",
@@ -735,246 +673,12 @@ func TestSSHCmdGetFsErrors(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
} }
func TestRsyncOptions(t *testing.T) {
permissions := make(map[string][]string)
permissions["/"] = []string{dataprovider.PermAny}
user := dataprovider.User{
BaseUser: sdk.BaseUser{
Permissions: permissions,
HomeDir: filepath.Clean(os.TempDir()),
},
}
conn := &Connection{
BaseConnection: common.NewBaseConnection("", common.ProtocolSFTP, "", "", user),
}
sshCmd := sshCommand{
command: "rsync",
connection: conn,
args: []string{"--server", "-vlogDtprze.iLsfxC", ".", "/"},
}
cmd, err := sshCmd.getSystemCommand()
assert.NoError(t, err)
assert.Equal(t, []string{"rsync", "--server", "-vlogDtprze.iLsfxC", "--safe-links", ".", user.HomeDir + string(os.PathSeparator)}, cmd.cmd.Args,
"--safe-links must be added if the user has the create symlinks permission")
permissions["/"] = []string{dataprovider.PermDownload, dataprovider.PermUpload, dataprovider.PermCreateDirs,
dataprovider.PermListItems, dataprovider.PermOverwrite, dataprovider.PermDelete, dataprovider.PermRename}
user.Permissions = permissions
conn = &Connection{
BaseConnection: common.NewBaseConnection("", common.ProtocolSFTP, "", "", user),
}
sshCmd = sshCommand{
command: "rsync",
connection: conn,
}
_, err = sshCmd.getSystemCommand()
assert.Error(t, err)
sshCmd = sshCommand{
command: "rsync",
connection: conn,
args: []string{"--server", "-vlogDtprze.iLsfxC", ".", "/"},
}
cmd, err = sshCmd.getSystemCommand()
assert.NoError(t, err)
assert.Equal(t, []string{"rsync", "--server", "-vlogDtprze.iLsfxC", "--munge-links", ".", user.HomeDir + string(os.PathSeparator)}, cmd.cmd.Args,
"--munge-links must be added if the user hasn't the create symlinks permission")
sshCmd.connection.User.VirtualFolders = append(sshCmd.connection.User.VirtualFolders, vfs.VirtualFolder{
BaseVirtualFolder: vfs.BaseVirtualFolder{
MappedPath: os.TempDir(),
},
VirtualPath: "/vdir",
})
_, err = sshCmd.getSystemCommand()
assert.EqualError(t, err, errUnsupportedConfig.Error())
}
func TestSystemCommandSizeForPath(t *testing.T) {
permissions := make(map[string][]string)
permissions["/"] = []string{dataprovider.PermAny}
user := dataprovider.User{
BaseUser: sdk.BaseUser{
Permissions: permissions,
HomeDir: os.TempDir(),
},
}
fs, err := user.GetFilesystem("123")
assert.NoError(t, err)
conn := &Connection{
BaseConnection: common.NewBaseConnection("", common.ProtocolSFTP, "", "", user),
}
sshCmd := sshCommand{
command: "rsync",
connection: conn,
args: []string{"--server", "-vlogDtprze.iLsfxC", ".", "/"},
}
_, _, err = sshCmd.getSizeForPath(fs, "missing path")
assert.NoError(t, err)
testDir := filepath.Join(os.TempDir(), "dir")
err = os.MkdirAll(testDir, os.ModePerm)
assert.NoError(t, err)
testFile := filepath.Join(testDir, "testfile")
err = os.WriteFile(testFile, []byte("test content"), os.ModePerm)
assert.NoError(t, err)
err = os.Symlink(testFile, testFile+".link")
assert.NoError(t, err)
numFiles, size, err := sshCmd.getSizeForPath(fs, testFile+".link")
assert.NoError(t, err)
assert.Equal(t, 0, numFiles)
assert.Equal(t, int64(0), size)
numFiles, size, err = sshCmd.getSizeForPath(fs, testFile)
assert.NoError(t, err)
assert.Equal(t, 1, numFiles)
assert.Equal(t, int64(12), size)
if runtime.GOOS != osWindows {
err = os.Chmod(testDir, 0001)
assert.NoError(t, err)
_, _, err = sshCmd.getSizeForPath(fs, testFile)
assert.Error(t, err)
err = os.Chmod(testDir, os.ModePerm)
assert.NoError(t, err)
}
err = os.RemoveAll(testDir)
assert.NoError(t, err)
}
func TestSystemCommandErrors(t *testing.T) {
buf := make([]byte, 65535)
stdErrBuf := make([]byte, 65535)
readErr := fmt.Errorf("test read error")
writeErr := fmt.Errorf("test write error")
mockSSHChannel := MockChannel{
Buffer: bytes.NewBuffer(buf),
StdErrBuffer: bytes.NewBuffer(stdErrBuf),
ReadError: nil,
WriteError: writeErr,
}
permissions := make(map[string][]string)
permissions["/"] = []string{dataprovider.PermAny}
homeDir := filepath.Join(os.TempDir(), "adir")
err := os.MkdirAll(homeDir, os.ModePerm)
assert.NoError(t, err)
err = os.WriteFile(filepath.Join(homeDir, "afile"), []byte("content"), os.ModePerm)
assert.NoError(t, err)
user := dataprovider.User{
BaseUser: sdk.BaseUser{
Permissions: permissions,
HomeDir: homeDir,
},
}
fs, err := user.GetFilesystem("123")
assert.NoError(t, err)
connection := &Connection{
BaseConnection: common.NewBaseConnection("", common.ProtocolSFTP, "", "", user),
channel: &mockSSHChannel,
}
var sshCmd sshCommand
if runtime.GOOS == osWindows {
sshCmd = sshCommand{
command: "dir",
connection: connection,
args: []string{"/"},
}
} else {
sshCmd = sshCommand{
command: "ls",
connection: connection,
args: []string{"/"},
}
}
systemCmd, err := sshCmd.getSystemCommand()
assert.NoError(t, err)
systemCmd.cmd.Dir = os.TempDir()
// FIXME: the command completes but the fake client is unable to read the response
// no error is reported in this case. We can see that the expected code is executed
// reading the test coverage
sshCmd.executeSystemCommand(systemCmd) //nolint:errcheck
mockSSHChannel = MockChannel{
Buffer: bytes.NewBuffer(buf),
StdErrBuffer: bytes.NewBuffer(stdErrBuf),
ReadError: readErr,
WriteError: nil,
}
sshCmd.connection.channel = &mockSSHChannel
baseTransfer := common.NewBaseTransfer(nil, sshCmd.connection.BaseConnection, nil, "", "", "",
common.TransferUpload, 0, 0, 0, 0, false, fs, dataprovider.TransferQuota{})
transfer := newTransfer(baseTransfer, nil, nil, nil)
destBuff := make([]byte, 65535)
dst := bytes.NewBuffer(destBuff)
_, err = transfer.copyFromReaderToWriter(dst, sshCmd.connection.channel)
assert.EqualError(t, err, readErr.Error())
mockSSHChannel = MockChannel{
Buffer: bytes.NewBuffer(buf),
StdErrBuffer: bytes.NewBuffer(stdErrBuf),
ReadError: nil,
WriteError: nil,
}
sshCmd.connection.channel = &mockSSHChannel
transfer.MaxWriteSize = 1
_, err = transfer.copyFromReaderToWriter(dst, sshCmd.connection.channel)
assert.True(t, transfer.Connection.IsQuotaExceededError(err))
mockSSHChannel = MockChannel{
Buffer: bytes.NewBuffer(buf),
StdErrBuffer: bytes.NewBuffer(stdErrBuf),
ReadError: nil,
WriteError: nil,
ShortWriteErr: true,
}
sshCmd.connection.channel = &mockSSHChannel
_, err = transfer.copyFromReaderToWriter(sshCmd.connection.channel, dst)
assert.EqualError(t, err, io.ErrShortWrite.Error())
transfer.MaxWriteSize = -1
_, err = transfer.copyFromReaderToWriter(sshCmd.connection.channel, dst)
assert.True(t, transfer.Connection.IsQuotaExceededError(err))
err = transfer.Close()
assert.Error(t, err)
baseTransfer = common.NewBaseTransfer(nil, sshCmd.connection.BaseConnection, nil, "", "", "",
common.TransferDownload, 0, 0, 0, 0, false, fs, dataprovider.TransferQuota{
AllowedDLSize: 1,
})
transfer = newTransfer(baseTransfer, nil, nil, nil)
mockSSHChannel = MockChannel{
Buffer: bytes.NewBuffer(buf),
StdErrBuffer: bytes.NewBuffer(stdErrBuf),
ReadError: nil,
WriteError: nil,
}
sshCmd.connection.channel = &mockSSHChannel
_, err = transfer.copyFromReaderToWriter(dst, sshCmd.connection.channel)
if assert.Error(t, err) {
assert.Contains(t, err.Error(), common.ErrReadQuotaExceeded.Error())
}
err = transfer.Close()
assert.Error(t, err)
err = os.RemoveAll(homeDir)
assert.NoError(t, err)
assert.Equal(t, int32(0), common.Connections.GetTotalTransfers())
}
func TestCommandGetFsError(t *testing.T) { func TestCommandGetFsError(t *testing.T) {
user := dataprovider.User{ user := dataprovider.User{
FsConfig: vfs.Filesystem{ FsConfig: vfs.Filesystem{
Provider: sdk.CryptedFilesystemProvider, Provider: sdk.CryptedFilesystemProvider,
}, },
} }
conn := &Connection{
BaseConnection: common.NewBaseConnection("", common.ProtocolSFTP, "", "", user),
}
sshCmd := sshCommand{
command: "rsync",
connection: conn,
args: []string{"--server", "-vlogDtprze.iLsfxC", ".", "/"},
}
_, err := sshCmd.getSystemCommand()
assert.Error(t, err)
buf := make([]byte, 65535) buf := make([]byte, 65535)
stdErrBuf := make([]byte, 65535) stdErrBuf := make([]byte, 65535)
@@ -983,7 +687,7 @@ func TestCommandGetFsError(t *testing.T) {
StdErrBuffer: bytes.NewBuffer(stdErrBuf), StdErrBuffer: bytes.NewBuffer(stdErrBuf),
ReadError: nil, ReadError: nil,
} }
conn = &Connection{ conn := &Connection{
BaseConnection: common.NewBaseConnection("", common.ProtocolSCP, "", "", user), BaseConnection: common.NewBaseConnection("", common.ProtocolSCP, "", "", user),
channel: &mockSSHChannel, channel: &mockSSHChannel,
} }
@@ -995,7 +699,7 @@ func TestCommandGetFsError(t *testing.T) {
}, },
} }
err = scpCommand.handleRecursiveUpload() err := scpCommand.handleRecursiveUpload()
assert.Error(t, err) assert.Error(t, err)
err = scpCommand.handleDownload("") err = scpCommand.handleDownload("")
assert.Error(t, err) assert.Error(t, err)
@@ -1910,40 +1614,6 @@ func TestCertCheckerInitErrors(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
} }
func TestSFTPSubSystem(t *testing.T) {
permissions := make(map[string][]string)
permissions["/"] = []string{dataprovider.PermAny}
user := &dataprovider.User{
BaseUser: sdk.BaseUser{
Permissions: permissions,
HomeDir: os.TempDir(),
},
}
user.FsConfig.Provider = sdk.AzureBlobFilesystemProvider
err := ServeSubSystemConnection(user, "connID", nil, nil)
assert.Error(t, err)
user.FsConfig.Provider = sdk.LocalFilesystemProvider
buf := make([]byte, 0, 4096)
stdErrBuf := make([]byte, 0, 4096)
mockSSHChannel := &MockChannel{
Buffer: bytes.NewBuffer(buf),
StdErrBuffer: bytes.NewBuffer(stdErrBuf),
}
// this is 327680 and it will result in packet too long error
_, err = mockSSHChannel.Write([]byte{0x00, 0x05, 0x00, 0x00, 0x00, 0x00})
assert.NoError(t, err)
err = ServeSubSystemConnection(user, "id", mockSSHChannel, mockSSHChannel)
assert.EqualError(t, err, "packet too long")
subsystemChannel := newSubsystemChannel(mockSSHChannel, mockSSHChannel)
n, err := subsystemChannel.Write([]byte{0x00})
assert.NoError(t, err)
assert.Equal(t, n, 1)
err = subsystemChannel.Close()
assert.NoError(t, err)
}
func TestRecoverer(t *testing.T) { func TestRecoverer(t *testing.T) {
c := Configuration{} c := Configuration{}
c.AcceptInboundConnection(nil, nil) c.AcceptInboundConnection(nil, nil)
@@ -2074,9 +1744,27 @@ func TestMaxUserSessions(t *testing.T) {
c := Configuration{} c := Configuration{}
c.handleSftpConnection(nil, connection) c.handleSftpConnection(nil, connection)
buf := make([]byte, 65535)
stdErrBuf := make([]byte, 65535)
mockSSHChannel := MockChannel{
Buffer: bytes.NewBuffer(buf),
StdErrBuffer: bytes.NewBuffer(stdErrBuf),
}
conn := &Connection{
BaseConnection: common.NewBaseConnection(xid.New().String(), common.ProtocolSFTP, "", "", dataprovider.User{
BaseUser: sdk.BaseUser{
Username: "user_max_sessions",
HomeDir: filepath.Clean(os.TempDir()),
MaxSessions: 1,
},
}),
channel: &mockSSHChannel,
}
sshCmd := sshCommand{ sshCmd := sshCommand{
command: "cd", command: "cd",
connection: connection, connection: conn,
} }
err = sshCmd.handle() err = sshCmd.handle()
if assert.Error(t, err) { if assert.Error(t, err) {
@@ -2085,17 +1773,14 @@ func TestMaxUserSessions(t *testing.T) {
scpCmd := scpCommand{ scpCmd := scpCommand{
sshCommand: sshCommand{ sshCommand: sshCommand{
command: "scp", command: "scp",
connection: connection, connection: conn,
}, },
} }
err = scpCmd.handle() err = scpCmd.handle()
if assert.Error(t, err) { if assert.Error(t, err) {
assert.Contains(t, err.Error(), "too many open sessions") assert.Contains(t, err.Error(), "too many open sessions")
} }
err = ServeSubSystemConnection(&connection.User, connection.ID, nil, nil)
if assert.Error(t, err) {
assert.Contains(t, err.Error(), "too many open sessions")
}
common.Connections.Remove(connection.GetID()) common.Connections.Remove(connection.GetID())
assert.Len(t, common.Connections.GetStats(""), 0) assert.Len(t, common.Connections.GetStats(""), 0)
assert.Equal(t, int32(0), common.Connections.GetTotalTransfers()) assert.Equal(t, int32(0), common.Connections.GetTotalTransfers())
@@ -2157,43 +1842,3 @@ func TestAuthenticationErrors(t *testing.T) {
assert.ErrorIs(t, err, sftpAuthError) assert.ErrorIs(t, err, sftpAuthError)
assert.NotErrorIs(t, err, util.ErrNotFound) assert.NotErrorIs(t, err, util.ErrNotFound)
} }
func TestRsyncArguments(t *testing.T) {
assert.False(t, canAcceptRsyncArgs(nil))
args := []string{"-e", "--server"}
assert.False(t, canAcceptRsyncArgs(args))
args = []string{"--server", "--sender", "-vlogDtpre.iLsfxCIvu", ".", "."}
assert.True(t, canAcceptRsyncArgs(args))
args = []string{"--server", "--sender", "--server", "-vlogDtpre.iLsfxCIvu", ".", "."}
assert.False(t, canAcceptRsyncArgs(args))
args = []string{"--server", "..", "/"}
assert.False(t, canAcceptRsyncArgs(args))
args = []string{"--server", ".", "/"}
assert.False(t, canAcceptRsyncArgs(args))
args = []string{"--server", "--sender", "-vlogDtpre.iLsfxCIvu", ".", "."}
assert.True(t, canAcceptRsyncArgs(args))
args = []string{"--server", "--sender", "-vlogDtpre.iLsfxCIvu", "--delete", ".", "/"}
assert.True(t, canAcceptRsyncArgs(args))
args = []string{"--server", "-vlogDtpre.iLsfxCIvu", "--delete", ".", "/"}
assert.True(t, canAcceptRsyncArgs(args))
args = []string{"--server", "-vlogDtpre.iLsfxCIvu", "--delete", "/", ".", "/"}
assert.False(t, canAcceptRsyncArgs(args))
args = []string{"--server", "--sender", "-vlogDtpre.iLsfxCIvu", ".", "path1", "path2"}
assert.False(t, canAcceptRsyncArgs(args))
args = []string{"--server", "--sender", "-vlogDtpre.iLsfxCIvu", "."}
assert.False(t, canAcceptRsyncArgs(args))
args = []string{"--sender", "-vlogDtpre.iLsfxCIvu", "--delete", ".", "/"}
assert.False(t, canAcceptRsyncArgs(args))
args = []string{"--server", "-vlogDtpre.", "--delete", ".", "/"}
assert.False(t, canAcceptRsyncArgs(args))
args = []string{"--server", "--sender", "-vlogDtpre.", "--delete", ".", "/"}
assert.False(t, canAcceptRsyncArgs(args))
args = []string{"--server", "--sender", "-e.iLsfxCIvu", ".", "/"}
assert.True(t, canAcceptRsyncArgs(args))
args = []string{"--server", "-vlogDtpre.iLsfxCIvu", "--delete", "/"}
assert.False(t, canAcceptRsyncArgs(args))
args = []string{"--server", "-vlogDtpre.iLsfxCIvu", "--delete", "--safe-links"}
assert.False(t, canAcceptRsyncArgs(args))
args = []string{"--server", "-vlogDtpre.iLsfxCIvu", "--unsupported-option", ".", "/"}
assert.False(t, canAcceptRsyncArgs(args))
}

View File

@@ -1,35 +0,0 @@
// Copyright (C) 2019 Nicola Murino
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published
// by the Free Software Foundation, version 3.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//go:build !windows
package sftpd
import (
"os/exec"
"testing"
"github.com/stretchr/testify/assert"
)
func TestWrapCmd(t *testing.T) {
cmd := exec.Command("ls")
cmd = wrapCmd(cmd, 3001, 3002)
assert.Equal(t, uint32(3001), cmd.SysProcAttr.Credential.Uid)
assert.Equal(t, uint32(3002), cmd.SysProcAttr.Credential.Gid)
cmd = exec.Command("cd")
cmd = wrapCmd(cmd, processUID, processGID)
assert.Nil(t, cmd.SysProcAttr)
}

View File

@@ -51,8 +51,9 @@ func (c *scpCommand) handle() (err error) {
} }
}() }()
if err := common.Connections.Add(c.connection); err != nil { if err := common.Connections.Add(c.connection); err != nil {
defer c.connection.CloseFS() //nolint:errcheck
logger.Info(logSender, "", "unable to add SCP connection: %v", err) logger.Info(logSender, "", "unable to add SCP connection: %v", err)
return err return c.sendErrorResponse(err)
} }
defer common.Connections.Remove(c.connection.GetID()) defer common.Connections.Remove(c.connection.GetID())

View File

@@ -712,6 +712,7 @@ func (c *Configuration) handleSftpConnection(channel ssh.Channel, connection *Co
} }
}() }()
if err := common.Connections.Add(connection); err != nil { if err := common.Connections.Add(connection); err != nil {
defer connection.CloseFS() //nolint:errcheck
errClose := connection.Disconnect() errClose := connection.Disconnect()
logger.Info(logSender, "", "unable to add connection: %v, close err: %v", err, errClose) logger.Info(logSender, "", "unable to add connection: %v, close err: %v", err, errClose)
return return

View File

@@ -31,10 +31,9 @@ const (
var ( var (
supportedSSHCommands = []string{"scp", "md5sum", "sha1sum", "sha256sum", "sha384sum", "sha512sum", "cd", "pwd", supportedSSHCommands = []string{"scp", "md5sum", "sha1sum", "sha256sum", "sha384sum", "sha512sum", "cd", "pwd",
"rsync", "sftpgo-copy", "sftpgo-remove"} "sftpgo-copy", "sftpgo-remove"}
defaultSSHCommands = []string{"md5sum", "sha1sum", "sha256sum", "cd", "pwd", "scp"} defaultSSHCommands = []string{"md5sum", "sha1sum", "sha256sum", "cd", "pwd", "scp"}
sshHashCommands = []string{"md5sum", "sha1sum", "sha256sum", "sha384sum", "sha512sum"} sshHashCommands = []string{"md5sum", "sha1sum", "sha256sum", "sha384sum", "sha512sum"}
systemCommands = []string{"rsync"}
serviceStatus ServiceStatus serviceStatus ServiceStatus
certKeyAlgoNames = map[string]string{ certKeyAlgoNames = map[string]string{
ssh.CertAlgoRSAv01: ssh.KeyAlgoRSA, ssh.CertAlgoRSAv01: ssh.KeyAlgoRSA,

View File

@@ -23,17 +23,12 @@ import (
"fmt" "fmt"
"hash" "hash"
"io" "io"
"os"
"os/exec"
"path"
"runtime/debug" "runtime/debug"
"slices" "slices"
"strings" "strings"
"sync"
"time" "time"
"github.com/google/shlex" "github.com/google/shlex"
"github.com/sftpgo/sdk"
"golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh"
"github.com/drakkan/sftpgo/v2/internal/common" "github.com/drakkan/sftpgo/v2/internal/common"
@@ -49,10 +44,6 @@ const (
sshCommandLogSender = "SSHCommand" sshCommandLogSender = "SSHCommand"
) )
var (
errUnsupportedConfig = errors.New("command unsupported for this configuration")
)
type sshCommand struct { type sshCommand struct {
command string command string
args []string args []string
@@ -60,32 +51,6 @@ type sshCommand struct {
startTime time.Time startTime time.Time
} }
type systemCommand struct {
cmd *exec.Cmd
fsPath string
quotaCheckPath string
fs vfs.Fs
}
func (c *systemCommand) GetSTDs() (io.WriteCloser, io.ReadCloser, io.ReadCloser, error) {
stdin, err := c.cmd.StdinPipe()
if err != nil {
return nil, nil, nil, err
}
stdout, err := c.cmd.StdoutPipe()
if err != nil {
stdin.Close()
return nil, nil, nil, err
}
stderr, err := c.cmd.StderrPipe()
if err != nil {
stdin.Close()
stdout.Close()
return nil, nil, nil, err
}
return stdin, stdout, stderr, nil
}
func processSSHCommand(payload []byte, connection *Connection, enabledSSHCommands []string) bool { func processSSHCommand(payload []byte, connection *Connection, enabledSSHCommands []string) bool {
var msg sshSubsystemExecMsg var msg sshSubsystemExecMsg
if err := ssh.Unmarshal(payload, &msg); err == nil { if err := ssh.Unmarshal(payload, &msg); err == nil {
@@ -134,20 +99,15 @@ func (c *sshCommand) handle() (err error) {
} }
}() }()
if err := common.Connections.Add(c.connection); err != nil { if err := common.Connections.Add(c.connection); err != nil {
defer c.connection.CloseFS() //nolint:errcheck
logger.Info(logSender, "", "unable to add SSH command connection: %v", err) logger.Info(logSender, "", "unable to add SSH command connection: %v", err)
return err return c.sendErrorResponse(err)
} }
defer common.Connections.Remove(c.connection.GetID()) defer common.Connections.Remove(c.connection.GetID())
c.connection.UpdateLastActivity() c.connection.UpdateLastActivity()
if slices.Contains(sshHashCommands, c.command) { if slices.Contains(sshHashCommands, c.command) {
return c.handleHashCommands() return c.handleHashCommands()
} else if slices.Contains(systemCommands, c.command) {
command, err := c.getSystemCommand()
if err != nil {
return c.sendErrorResponse(err)
}
return c.executeSystemCommand(command)
} else if c.command == "cd" { } else if c.command == "cd" {
c.sendExitStatus(nil) c.sendExitStatus(nil)
} else if c.command == "pwd" { } else if c.command == "pwd" {
@@ -190,15 +150,6 @@ func (c *sshCommand) handleSFTPGoRemove() error {
return nil return nil
} }
func (c *sshCommand) updateQuota(sshDestPath string, filesNum int, filesSize int64) {
vfolder, err := c.connection.User.GetVirtualFolderForPath(sshDestPath)
if err == nil {
dataprovider.UpdateUserFolderQuota(&vfolder, &c.connection.User, filesNum, filesSize, false)
return
}
dataprovider.UpdateUserQuota(&c.connection.User, filesNum, filesSize, false) //nolint:errcheck
}
func (c *sshCommand) handleHashCommands() error { func (c *sshCommand) handleHashCommands() error {
var h hash.Hash var h hash.Hash
switch c.command { switch c.command {
@@ -247,299 +198,6 @@ func (c *sshCommand) handleHashCommands() error {
return nil return nil
} }
func (c *sshCommand) executeSystemCommand(command systemCommand) error { //nolint:gocyclo
sshDestPath := c.getDestPath()
if !c.isLocalPath(sshDestPath) {
return c.sendErrorResponse(errUnsupportedConfig)
}
if err := common.Connections.IsNewTransferAllowed(c.connection.User.Username); err != nil {
err := fmt.Errorf("denying command due to transfer count limits")
return c.sendErrorResponse(err)
}
diskQuota, transferQuota := c.connection.HasSpace(true, false, command.quotaCheckPath)
if !diskQuota.HasSpace || !transferQuota.HasUploadSpace() || !transferQuota.HasDownloadSpace() {
return c.sendErrorResponse(common.ErrQuotaExceeded)
}
perms := []string{dataprovider.PermDownload, dataprovider.PermUpload, dataprovider.PermCreateDirs, dataprovider.PermListItems,
dataprovider.PermOverwrite, dataprovider.PermDelete}
if !c.connection.User.HasRecursivePerms(perms, sshDestPath) {
return c.sendErrorResponse(c.connection.GetPermissionDeniedError())
}
initialFiles, initialSize, err := c.getSizeForPath(command.fs, command.fsPath)
if err != nil {
return c.sendErrorResponse(err)
}
stdin, stdout, stderr, err := command.GetSTDs()
if err != nil {
return c.sendErrorResponse(err)
}
err = command.cmd.Start()
if err != nil {
return c.sendErrorResponse(err)
}
closeCmdOnError := func() {
c.connection.Log(logger.LevelDebug, "kill cmd: %q and close ssh channel after read or write error",
c.connection.command)
killerr := command.cmd.Process.Kill()
closerr := c.connection.channel.Close()
c.connection.Log(logger.LevelDebug, "kill cmd error: %v close channel error: %v", killerr, closerr)
}
var once sync.Once
commandResponse := make(chan bool)
remainingQuotaSize := diskQuota.GetRemainingSize()
go func() {
defer stdin.Close()
baseTransfer := common.NewBaseTransfer(nil, c.connection.BaseConnection, nil, command.fsPath, command.fsPath, sshDestPath,
common.TransferUpload, 0, 0, remainingQuotaSize, 0, false, command.fs, transferQuota)
transfer := newTransfer(baseTransfer, nil, nil, nil)
w, e := transfer.copyFromReaderToWriter(stdin, c.connection.channel)
c.connection.Log(logger.LevelDebug, "command: %q, copy from remote command to sdtin ended, written: %v, "+
"initial remaining quota: %v, err: %v", c.connection.command, w, remainingQuotaSize, e)
if e != nil {
once.Do(closeCmdOnError)
}
}()
go func() {
baseTransfer := common.NewBaseTransfer(nil, c.connection.BaseConnection, nil, command.fsPath, command.fsPath, sshDestPath,
common.TransferDownload, 0, 0, 0, 0, false, command.fs, transferQuota)
transfer := newTransfer(baseTransfer, nil, nil, nil)
w, e := transfer.copyFromReaderToWriter(c.connection.channel, stdout)
c.connection.Log(logger.LevelDebug, "command: %q, copy from sdtout to remote command ended, written: %v err: %v",
c.connection.command, w, e)
if e != nil {
once.Do(closeCmdOnError)
}
commandResponse <- true
}()
go func() {
baseTransfer := common.NewBaseTransfer(nil, c.connection.BaseConnection, nil, command.fsPath, command.fsPath, sshDestPath,
common.TransferDownload, 0, 0, 0, 0, false, command.fs, transferQuota)
transfer := newTransfer(baseTransfer, nil, nil, nil)
w, e := transfer.copyFromReaderToWriter(c.connection.channel.(ssh.Channel).Stderr(), stderr)
c.connection.Log(logger.LevelDebug, "command: %q, copy from sdterr to remote command ended, written: %v err: %v",
c.connection.command, w, e)
// os.ErrClosed means that the command is finished so we don't need to do anything
if (e != nil && !errors.Is(e, os.ErrClosed)) || w > 0 {
once.Do(closeCmdOnError)
}
}()
<-commandResponse
err = command.cmd.Wait()
c.sendExitStatus(err)
numFiles, dirSize, errSize := c.getSizeForPath(command.fs, command.fsPath)
if errSize == nil {
c.updateQuota(sshDestPath, numFiles-initialFiles, dirSize-initialSize)
}
c.connection.Log(logger.LevelDebug, "command %q finished for path %q, initial files %v initial size %v "+
"current files %v current size %v size err: %v", c.connection.command, command.fsPath, initialFiles, initialSize,
numFiles, dirSize, errSize)
return c.connection.GetFsError(command.fs, err)
}
func (c *sshCommand) isSystemCommandAllowed() error {
sshDestPath := c.getDestPath()
if c.connection.User.IsVirtualFolder(sshDestPath) {
// overlapped virtual path are not allowed
return nil
}
if c.connection.User.HasVirtualFoldersInside(sshDestPath) {
c.connection.Log(logger.LevelDebug, "command %q is not allowed, path %q has virtual folders inside it, user %q",
c.command, sshDestPath, c.connection.User.Username)
return errUnsupportedConfig
}
for _, f := range c.connection.User.Filters.FilePatterns {
if f.Path == sshDestPath {
c.connection.Log(logger.LevelDebug,
"command %q is not allowed inside folders with file patterns filters %q user %q",
c.command, sshDestPath, c.connection.User.Username)
return errUnsupportedConfig
}
if len(sshDestPath) > len(f.Path) {
if strings.HasPrefix(sshDestPath, f.Path+"/") || f.Path == "/" {
c.connection.Log(logger.LevelDebug,
"command %q is not allowed it includes folders with file patterns filters %q user %q",
c.command, sshDestPath, c.connection.User.Username)
return errUnsupportedConfig
}
}
if len(sshDestPath) < len(f.Path) {
if strings.HasPrefix(sshDestPath+"/", f.Path) || sshDestPath == "/" {
c.connection.Log(logger.LevelDebug,
"command %q is not allowed inside folder with file patterns filters %q user %q",
c.command, sshDestPath, c.connection.User.Username)
return errUnsupportedConfig
}
}
}
return nil
}
func (c *sshCommand) getSystemCommand() (systemCommand, error) {
command := systemCommand{
cmd: nil,
fs: nil,
fsPath: "",
quotaCheckPath: "",
}
if err := common.CheckClosing(); err != nil {
return command, err
}
args := make([]string, len(c.args))
copy(args, c.args)
var fsPath, quotaPath string
sshPath := c.getDestPath()
fs, err := c.connection.User.GetFilesystemForPath(sshPath, c.connection.ID)
if err != nil {
return command, err
}
if len(c.args) > 0 {
var err error
fsPath, err = fs.ResolvePath(sshPath)
if err != nil {
return command, c.connection.GetFsError(fs, err)
}
quotaPath = sshPath
fi, err := fs.Stat(fsPath)
if err == nil && fi.IsDir() {
// if the target is an existing dir the command will write inside this dir
// so we need to check the quota for this directory and not its parent dir
quotaPath = path.Join(sshPath, "fakecontent")
}
if strings.HasSuffix(sshPath, "/") && !strings.HasSuffix(fsPath, string(os.PathSeparator)) {
fsPath += string(os.PathSeparator)
c.connection.Log(logger.LevelDebug, "path separator added to fsPath %q", fsPath)
}
args = args[:len(args)-1]
args = append(args, fsPath)
}
if err := c.isSystemCommandAllowed(); err != nil {
return command, errUnsupportedConfig
}
if c.command == "rsync" {
if !canAcceptRsyncArgs(args) {
c.connection.Log(logger.LevelWarn, "invalid rsync command, args: %+v", args)
return command, errors.New("invalid or unsupported rsync command")
}
// we cannot avoid that rsync creates symlinks so if the user has the permission
// to create symlinks we add the option --safe-links to the received rsync command if
// it is not already set. This should prevent to create symlinks that point outside
// the home dir.
// If the user cannot create symlinks we add the option --munge-links, if it is not
// already set. This should make symlinks unusable (but manually recoverable)
if c.connection.User.HasPerm(dataprovider.PermCreateSymlinks, c.getDestPath()) {
if !slices.Contains(args, "--safe-links") {
args = slices.Insert(args, len(args)-2, "--safe-links")
}
} else {
if !slices.Contains(args, "--munge-links") {
args = slices.Insert(args, len(args)-2, "--munge-links")
}
}
}
c.connection.Log(logger.LevelDebug, "new system command %q, with args: %+v fs path %q quota check path %q",
c.command, args, fsPath, quotaPath)
cmd := exec.Command(c.command, args...)
uid := c.connection.User.GetUID()
gid := c.connection.User.GetGID()
cmd = wrapCmd(cmd, uid, gid)
command.cmd = cmd
command.fsPath = fsPath
command.quotaCheckPath = quotaPath
command.fs = fs
return command, nil
}
var (
acceptedRsyncOptions = []string{
"--existing",
"--ignore-existing",
"--remove-source-files",
"--delete",
"--delete-before",
"--delete-during",
"--delete-delay",
"--delete-after",
"--delete-excluded",
"--ignore-errors",
"--force",
"--partial",
"--delay-updates",
"--size-only",
"--blocking-io",
"--stats",
"--progress",
"--list-only",
"--dry-run",
}
)
func canAcceptRsyncArgs(args []string) bool {
// We support the following formats:
//
// rsync --server -vlogDtpre.iLsfxCIvu --supported-options . ARG # push
// rsync --server --sender -vlogDtpre.iLsfxCIvu --supported-options . ARG # pull
//
// Then some options with a single dash and containing "e." followed by
// supported options, listed in acceptedRsyncOptions, with double dash then
// dot and a finally single argument specifying the path to operate on.
idx := 0
if len(args) < 4 {
return false
}
// The first argument must be --server.
if args[idx] != "--server" {
return false
}
idx++
// The second argument must be --sender or an argument starting with a
// single dash and containing "e."
if args[idx] == "--sender" {
idx++
}
// Check that this argument starts with a dash and contains e. but does not
// end with e.
if !strings.HasPrefix(args[idx], "-") || strings.HasPrefix(args[idx], "--") ||
!strings.Contains(args[idx], "e.") || strings.HasSuffix(args[idx], "e.") {
return false
}
idx++
// We now expect optional supported options like --delete or a dot followed
// by the path to operate on. We don't support multiple paths in sender
// mode.
if len(args) < idx+2 {
return false
}
// A dot is required we'll check the expected position later.
if !slices.Contains(args, ".") {
return false
}
for _, arg := range args[idx:] {
if slices.Contains(acceptedRsyncOptions, arg) {
idx++
} else {
if arg == "." {
idx++
break
}
// Unsupported argument.
return false
}
}
return len(args) == idx+1
}
// for the supported commands, the destination path, if any, is the last argument // for the supported commands, the destination path, if any, is the last argument
func (c *sshCommand) getDestPath() string { func (c *sshCommand) getDestPath() string {
if len(c.args) == 0 { if len(c.args) == 0 {
@@ -578,37 +236,6 @@ func (c *sshCommand) getRemovePath() (string, error) {
return sshDestPath, nil return sshDestPath, nil
} }
func (c *sshCommand) isLocalPath(virtualPath string) bool {
folder, err := c.connection.User.GetVirtualFolderForPath(virtualPath)
if err != nil {
return c.connection.User.FsConfig.Provider == sdk.LocalFilesystemProvider
}
return folder.FsConfig.Provider == sdk.LocalFilesystemProvider
}
func (c *sshCommand) getSizeForPath(fs vfs.Fs, name string) (int, int64, error) {
if dataprovider.GetQuotaTracking() > 0 {
fi, err := fs.Lstat(name)
if err != nil {
if fs.IsNotExist(err) {
return 0, 0, nil
}
c.connection.Log(logger.LevelDebug, "unable to stat %q error: %v", name, err)
return 0, 0, err
}
if fi.IsDir() {
files, size, err := fs.GetDirSize(name)
if err != nil {
c.connection.Log(logger.LevelDebug, "unable to get size for dir %q error: %v", name, err)
}
return files, size, err
} else if fi.Mode().IsRegular() {
return 1, fi.Size(), nil
}
}
return 0, 0, nil
}
func (c *sshCommand) sendErrorResponse(err error) error { func (c *sshCommand) sendErrorResponse(err error) error {
errorString := fmt.Sprintf("%v: %v %v\n", c.command, c.getDestPath(), err) errorString := fmt.Sprintf("%v: %v %v\n", c.command, c.getDestPath(), err)
c.connection.channel.Write([]byte(errorString)) //nolint:errcheck c.connection.channel.Write([]byte(errorString)) //nolint:errcheck

View File

@@ -1,87 +0,0 @@
// Copyright (C) 2019 Nicola Murino
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published
// by the Free Software Foundation, version 3.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package sftpd
import (
"io"
"net"
"github.com/pkg/sftp"
"github.com/drakkan/sftpgo/v2/internal/common"
"github.com/drakkan/sftpgo/v2/internal/dataprovider"
"github.com/drakkan/sftpgo/v2/internal/logger"
)
type subsystemChannel struct {
reader io.Reader
writer io.Writer
}
func (s *subsystemChannel) Read(p []byte) (int, error) {
return s.reader.Read(p)
}
func (s *subsystemChannel) Write(p []byte) (int, error) {
return s.writer.Write(p)
}
func (s *subsystemChannel) Close() error {
return nil
}
func newSubsystemChannel(reader io.Reader, writer io.Writer) *subsystemChannel {
return &subsystemChannel{
reader: reader,
writer: writer,
}
}
// ServeSubSystemConnection handles a connection as SSH subsystem
func ServeSubSystemConnection(user *dataprovider.User, connectionID string, reader io.Reader, writer io.Writer) error {
err := user.CheckFsRoot(connectionID)
if err != nil {
errClose := user.CloseFs()
logger.Warn(logSender, connectionID, "unable to check fs root: %v close fs error: %v", err, errClose)
return err
}
connection := &Connection{
BaseConnection: common.NewBaseConnection(connectionID, common.ProtocolSFTP, "", "", *user),
ClientVersion: "",
RemoteAddr: &net.IPAddr{},
LocalAddr: &net.IPAddr{},
channel: newSubsystemChannel(reader, writer),
}
err = common.Connections.Add(connection)
if err != nil {
errClose := user.CloseFs()
logger.Warn(logSender, connectionID, "unable to add connection: %v close fs error: %v", err, errClose)
return err
}
defer common.Connections.Remove(connection.GetID())
dataprovider.UpdateLastLogin(user)
sftp.SetSFTPExtensions(sftpExtensions...) //nolint:errcheck
server := sftp.NewRequestServer(connection.channel, sftp.Handlers{
FileGet: connection,
FilePut: connection,
FileCmd: connection,
FileList: connection,
})
defer server.Close()
return server.Serve()
}

View File

@@ -19,7 +19,6 @@ import (
"io" "io"
"github.com/drakkan/sftpgo/v2/internal/common" "github.com/drakkan/sftpgo/v2/internal/common"
"github.com/drakkan/sftpgo/v2/internal/metric"
"github.com/drakkan/sftpgo/v2/internal/vfs" "github.com/drakkan/sftpgo/v2/internal/vfs"
) )
@@ -192,63 +191,3 @@ func (t *transfer) setFinished() error {
t.isFinished = true t.isFinished = true
return nil return nil
} }
// used for ssh commands.
// It reads from src until EOF so it does not treat an EOF from Read as an error to be reported.
// EOF from Write is reported as error
func (t *transfer) copyFromReaderToWriter(dst io.Writer, src io.Reader) (int64, error) {
defer t.Connection.RemoveTransfer(t)
var written int64
var err error
if t.MaxWriteSize < 0 {
return 0, common.ErrQuotaExceeded
}
isDownload := t.GetType() == common.TransferDownload
buf := make([]byte, 32768)
for {
t.Connection.UpdateLastActivity()
nr, er := src.Read(buf)
if nr > 0 {
nw, ew := dst.Write(buf[0:nr])
if nw > 0 {
written += int64(nw)
if isDownload {
t.BytesSent.Store(written)
if errCheck := t.CheckRead(); errCheck != nil {
err = errCheck
break
}
} else {
t.BytesReceived.Store(written)
if errCheck := t.CheckWrite(); errCheck != nil {
err = errCheck
break
}
}
}
if ew != nil {
err = ew
break
}
if nr != nw {
err = io.ErrShortWrite
break
}
}
if er != nil {
if er != io.EOF {
err = er
}
break
}
t.HandleThrottle()
}
t.ErrTransfer = err
if written > 0 || err != nil {
metric.TransferCompleted(t.BytesSent.Load(), t.BytesReceived.Load(), t.GetType(),
t.ErrTransfer, vfs.IsSFTPFs(t.Fs))
}
return written, err
}

View File

@@ -5385,7 +5385,7 @@ components:
max_upload_file_size: max_upload_file_size:
type: integer type: integer
format: int64 format: int64
description: 'maximum allowed size, as bytes, for a single file upload. The upload will be aborted if/when the size of the file being sent exceeds this limit. 0 means unlimited. This restriction does not apply for SSH system commands such as `git` and `rsync`' description: 'maximum allowed size, as bytes, for a single file upload. The upload will be aborted if/when the size of the file being sent exceeds this limit. 0 means unlimited'
tls_username: tls_username:
type: string type: string
description: 'defines the TLS certificate field to use as username. For FTP clients it must match the name provided using the "USER" command. For WebDAV, if no username is provided, the CN will be used as username. For WebDAV clients it must match the implicit or provided username. Ignored if mutual TLS is disabled. Currently the only supported value is `CommonName`' description: 'defines the TLS certificate field to use as username. For FTP clients it must match the name provided using the "USER" command. For WebDAV, if no username is provided, the CN will be used as username. For WebDAV clients it must match the implicit or provided username. Ignored if mutual TLS is disabled. Currently the only supported value is `CommonName`'