Bläddra i källkod

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 <[email protected]>
Nicola Murino 3 veckor sedan
förälder
incheckning
35525e22e9

+ 0 - 13
internal/common/common_test.go

@@ -1412,19 +1412,6 @@ func TestUserPerms(t *testing.T) {
 	u.Permissions["/"] = []string{dataprovider.PermDeleteDirs, dataprovider.PermRenameFiles, dataprovider.PermRenameDirs}
 	assert.False(t, u.HasPermsDeleteAll("/"))
 	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) {

+ 0 - 21
internal/dataprovider/user.go

@@ -876,27 +876,6 @@ func (u *User) HasAnyPerm(permissions []string, path string) bool {
 	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
 func (u *User) HasPerms(permissions []string, path string) bool {
 	perms := u.GetPermissionsForPath(path)

+ 0 - 37
internal/sftpd/cmd_unix.go

@@ -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
-}

+ 0 - 23
internal/sftpd/cmd_windows.go

@@ -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
-}

+ 24 - 379
internal/sftpd/internal_test.go

@@ -538,33 +538,6 @@ func TestSSHCommandErrors(t *testing.T) {
 	_, err = cmd.connection.User.GetFilesystem("123")
 	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{
 		command:    "sftpgo-remove",
 		connection: &connection,
@@ -581,23 +554,6 @@ func TestSSHCommandErrors(t *testing.T) {
 	err = cmd.handle()
 	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)
 	assert.NoError(t, err)
 }
@@ -638,14 +594,6 @@ func TestCommandsWithExtensionsFilter(t *testing.T) {
 	}
 	err := cmd.handleHashCommands()
 	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) {
@@ -676,17 +624,7 @@ func TestSSHCommandsRemoteFs(t *testing.T) {
 		args:       []string{},
 	}
 
-	command, err := cmd.getSystemCommand()
-	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()
+	err := cmd.handleSFTPGoCopy()
 	assert.Error(t, err)
 	cmd = sshCommand{
 		command:    "sftpgo-remove",
@@ -735,246 +673,12 @@ func TestSSHCmdGetFsErrors(t *testing.T) {
 	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) {
 	user := dataprovider.User{
 		FsConfig: vfs.Filesystem{
 			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)
 	stdErrBuf := make([]byte, 65535)
@@ -983,7 +687,7 @@ func TestCommandGetFsError(t *testing.T) {
 		StdErrBuffer: bytes.NewBuffer(stdErrBuf),
 		ReadError:    nil,
 	}
-	conn = &Connection{
+	conn := &Connection{
 		BaseConnection: common.NewBaseConnection("", common.ProtocolSCP, "", "", user),
 		channel:        &mockSSHChannel,
 	}
@@ -995,7 +699,7 @@ func TestCommandGetFsError(t *testing.T) {
 		},
 	}
 
-	err = scpCommand.handleRecursiveUpload()
+	err := scpCommand.handleRecursiveUpload()
 	assert.Error(t, err)
 	err = scpCommand.handleDownload("")
 	assert.Error(t, err)
@@ -1910,40 +1614,6 @@ func TestCertCheckerInitErrors(t *testing.T) {
 	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) {
 	c := Configuration{}
 	c.AcceptInboundConnection(nil, nil)
@@ -2074,9 +1744,27 @@ func TestMaxUserSessions(t *testing.T) {
 	c := Configuration{}
 	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{
 		command:    "cd",
-		connection: connection,
+		connection: conn,
 	}
 	err = sshCmd.handle()
 	if assert.Error(t, err) {
@@ -2085,17 +1773,14 @@ func TestMaxUserSessions(t *testing.T) {
 	scpCmd := scpCommand{
 		sshCommand: sshCommand{
 			command:    "scp",
-			connection: connection,
+			connection: conn,
 		},
 	}
 	err = scpCmd.handle()
 	if assert.Error(t, err) {
 		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())
 	assert.Len(t, common.Connections.GetStats(""), 0)
 	assert.Equal(t, int32(0), common.Connections.GetTotalTransfers())
@@ -2157,43 +1842,3 @@ func TestAuthenticationErrors(t *testing.T) {
 	assert.ErrorIs(t, err, sftpAuthError)
 	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))
-}

+ 0 - 35
internal/sftpd/internal_unix_test.go

@@ -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)
-}

+ 2 - 1
internal/sftpd/scp.go

@@ -51,8 +51,9 @@ func (c *scpCommand) handle() (err error) {
 		}
 	}()
 	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)
-		return err
+		return c.sendErrorResponse(err)
 	}
 	defer common.Connections.Remove(c.connection.GetID())
 

+ 1 - 0
internal/sftpd/server.go

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

+ 1 - 2
internal/sftpd/sftpd.go

@@ -31,10 +31,9 @@ const (
 
 var (
 	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"}
 	sshHashCommands    = []string{"md5sum", "sha1sum", "sha256sum", "sha384sum", "sha512sum"}
-	systemCommands     = []string{"rsync"}
 	serviceStatus      ServiceStatus
 	certKeyAlgoNames   = map[string]string{
 		ssh.CertAlgoRSAv01:         ssh.KeyAlgoRSA,

+ 2 - 375
internal/sftpd/ssh_cmd.go

@@ -23,17 +23,12 @@ import (
 	"fmt"
 	"hash"
 	"io"
-	"os"
-	"os/exec"
-	"path"
 	"runtime/debug"
 	"slices"
 	"strings"
-	"sync"
 	"time"
 
 	"github.com/google/shlex"
-	"github.com/sftpgo/sdk"
 	"golang.org/x/crypto/ssh"
 
 	"github.com/drakkan/sftpgo/v2/internal/common"
@@ -49,10 +44,6 @@ const (
 	sshCommandLogSender = "SSHCommand"
 )
 
-var (
-	errUnsupportedConfig = errors.New("command unsupported for this configuration")
-)
-
 type sshCommand struct {
 	command    string
 	args       []string
@@ -60,32 +51,6 @@ type sshCommand struct {
 	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 {
 	var msg sshSubsystemExecMsg
 	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 {
+		defer c.connection.CloseFS() //nolint:errcheck
 		logger.Info(logSender, "", "unable to add SSH command connection: %v", err)
-		return err
+		return c.sendErrorResponse(err)
 	}
 	defer common.Connections.Remove(c.connection.GetID())
 
 	c.connection.UpdateLastActivity()
 	if slices.Contains(sshHashCommands, c.command) {
 		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" {
 		c.sendExitStatus(nil)
 	} else if c.command == "pwd" {
@@ -190,15 +150,6 @@ func (c *sshCommand) handleSFTPGoRemove() error {
 	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 {
 	var h hash.Hash
 	switch c.command {
@@ -247,299 +198,6 @@ func (c *sshCommand) handleHashCommands() error {
 	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
 func (c *sshCommand) getDestPath() string {
 	if len(c.args) == 0 {
@@ -578,37 +236,6 @@ func (c *sshCommand) getRemovePath() (string, error) {
 	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 {
 	errorString := fmt.Sprintf("%v: %v %v\n", c.command, c.getDestPath(), err)
 	c.connection.channel.Write([]byte(errorString)) //nolint:errcheck

+ 0 - 87
internal/sftpd/subsystem.go

@@ -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()
-}

+ 0 - 61
internal/sftpd/transfer.go

@@ -19,7 +19,6 @@ import (
 	"io"
 
 	"github.com/drakkan/sftpgo/v2/internal/common"
-	"github.com/drakkan/sftpgo/v2/internal/metric"
 	"github.com/drakkan/sftpgo/v2/internal/vfs"
 )
 
@@ -192,63 +191,3 @@ func (t *transfer) setFinished() error {
 	t.isFinished = true
 	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
-}

+ 1 - 1
openapi/openapi.yaml

@@ -5385,7 +5385,7 @@ components:
         max_upload_file_size:
           type: integer
           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:
           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`'