mirror of
https://github.com/drakkan/sftpgo.git
synced 2025-12-07 06:40:54 +03:00
SSH system commands: allow git and rsync inside virtual folders
This commit is contained in:
174
sftpd/ssh_cmd.go
174
sftpd/ssh_cmd.go
@@ -42,8 +42,9 @@ type sshCommand struct {
|
||||
}
|
||||
|
||||
type systemCommand struct {
|
||||
cmd *exec.Cmd
|
||||
realPath string
|
||||
cmd *exec.Cmd
|
||||
fsPath string
|
||||
quotaCheckPath string
|
||||
}
|
||||
|
||||
func processSSHCommand(payload []byte, connection *Connection, channel ssh.Channel, enabledSSHCommands []string) bool {
|
||||
@@ -299,15 +300,21 @@ func (c *sshCommand) executeSystemCommand(command systemCommand) error {
|
||||
if !vfs.IsLocalOsFs(c.connection.fs) {
|
||||
return c.sendErrorResponse(errUnsupportedConfig)
|
||||
}
|
||||
if c.connection.User.QuotaFiles > 0 && c.connection.User.UsedQuotaFiles > c.connection.User.QuotaFiles {
|
||||
sshDestPath := c.getDestPath()
|
||||
if !c.connection.hasSpace(true, command.quotaCheckPath) {
|
||||
return c.sendErrorResponse(errQuotaExceeded)
|
||||
}
|
||||
perms := []string{dataprovider.PermDownload, dataprovider.PermUpload, dataprovider.PermCreateDirs, dataprovider.PermListItems,
|
||||
dataprovider.PermOverwrite, dataprovider.PermDelete, dataprovider.PermRename}
|
||||
if !c.connection.User.HasPerms(perms, c.getDestPath()) {
|
||||
dataprovider.PermOverwrite, dataprovider.PermDelete}
|
||||
if !c.connection.User.HasPerms(perms, sshDestPath) {
|
||||
return c.sendErrorResponse(errPermissionDenied)
|
||||
}
|
||||
|
||||
initialFiles, initialSize, err := c.getSizeForPath(command.fsPath)
|
||||
if err != nil {
|
||||
return c.sendErrorResponse(err)
|
||||
}
|
||||
|
||||
stdin, err := command.cmd.StdinPipe()
|
||||
if err != nil {
|
||||
return c.sendErrorResponse(err)
|
||||
@@ -337,13 +344,10 @@ func (c *sshCommand) executeSystemCommand(command systemCommand) error {
|
||||
|
||||
go func() {
|
||||
defer stdin.Close()
|
||||
remainingQuotaSize := int64(0)
|
||||
if c.connection.User.QuotaSize > 0 {
|
||||
remainingQuotaSize = c.connection.User.QuotaSize - c.connection.User.UsedQuotaSize
|
||||
}
|
||||
remainingQuotaSize := c.connection.User.GetRemaingQuotaSize(sshDestPath)
|
||||
transfer := Transfer{
|
||||
file: nil,
|
||||
path: command.realPath,
|
||||
path: command.fsPath,
|
||||
start: time.Now(),
|
||||
bytesSent: 0,
|
||||
bytesReceived: 0,
|
||||
@@ -371,7 +375,7 @@ func (c *sshCommand) executeSystemCommand(command systemCommand) error {
|
||||
go func() {
|
||||
transfer := Transfer{
|
||||
file: nil,
|
||||
path: command.realPath,
|
||||
path: command.fsPath,
|
||||
start: time.Now(),
|
||||
bytesSent: 0,
|
||||
bytesReceived: 0,
|
||||
@@ -400,7 +404,7 @@ func (c *sshCommand) executeSystemCommand(command systemCommand) error {
|
||||
go func() {
|
||||
transfer := Transfer{
|
||||
file: nil,
|
||||
path: command.realPath,
|
||||
path: command.fsPath,
|
||||
start: time.Now(),
|
||||
bytesSent: 0,
|
||||
bytesReceived: 0,
|
||||
@@ -429,38 +433,48 @@ func (c *sshCommand) executeSystemCommand(command systemCommand) error {
|
||||
<-commandResponse
|
||||
err = command.cmd.Wait()
|
||||
c.sendExitStatus(err)
|
||||
c.rescanHomeDir() //nolint:errcheck
|
||||
|
||||
numFiles, dirSize, errSize := c.getSizeForPath(command.fsPath)
|
||||
if errSize == nil {
|
||||
c.updateQuota(sshDestPath, numFiles-initialFiles, dirSize-initialSize)
|
||||
}
|
||||
c.connection.Log(logger.LevelDebug, logSenderSSH, "command %#v finished for path %#v, 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 err
|
||||
}
|
||||
|
||||
func (c *sshCommand) checkGitAllowed() error {
|
||||
gitPath := c.getDestPath()
|
||||
for _, v := range c.connection.User.VirtualFolders {
|
||||
if v.VirtualPath == gitPath {
|
||||
c.connection.Log(logger.LevelDebug, logSenderSSH, "git is not supported inside virtual folder %#v user %#v",
|
||||
gitPath, c.connection.User.Username)
|
||||
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, logSenderSSH, "command %#v is not allowed, path %#v has virtual folders inside it, user %#v",
|
||||
c.command, sshDestPath, c.connection.User.Username)
|
||||
return errUnsupportedConfig
|
||||
}
|
||||
for _, f := range c.connection.User.Filters.FileExtensions {
|
||||
if f.Path == sshDestPath {
|
||||
c.connection.Log(logger.LevelDebug, logSenderSSH,
|
||||
"command %#v is not allowed inside folders with files extensions filters %#v user %#v",
|
||||
c.command, sshDestPath, c.connection.User.Username)
|
||||
return errUnsupportedConfig
|
||||
}
|
||||
if len(gitPath) > len(v.VirtualPath) {
|
||||
if strings.HasPrefix(gitPath, v.VirtualPath+"/") {
|
||||
c.connection.Log(logger.LevelDebug, logSenderSSH, "git is not supported inside virtual folder %#v user %#v",
|
||||
gitPath, c.connection.User.Username)
|
||||
if len(sshDestPath) > len(f.Path) {
|
||||
if strings.HasPrefix(sshDestPath, f.Path+"/") || f.Path == "/" {
|
||||
c.connection.Log(logger.LevelDebug, logSenderSSH,
|
||||
"command %#v is not allowed it includes folders with files extensions filters %#v user %#v",
|
||||
c.command, sshDestPath, c.connection.User.Username)
|
||||
return errUnsupportedConfig
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, f := range c.connection.User.Filters.FileExtensions {
|
||||
if f.Path == gitPath {
|
||||
c.connection.Log(logger.LevelDebug, logSenderSSH,
|
||||
"git is not supported inside folder with files extensions filters %#v user %#v", gitPath,
|
||||
c.connection.User.Username)
|
||||
return errUnsupportedConfig
|
||||
}
|
||||
if len(gitPath) > len(f.Path) {
|
||||
if strings.HasPrefix(gitPath, f.Path+"/") || f.Path == "/" {
|
||||
if len(sshDestPath) < len(f.Path) {
|
||||
if strings.HasPrefix(sshDestPath+"/", f.Path) || sshDestPath == "/" {
|
||||
c.connection.Log(logger.LevelDebug, logSenderSSH,
|
||||
"git is not supported inside folder with files extensions filters %#v user %#v", gitPath,
|
||||
c.connection.User.Username)
|
||||
"command %#v is not allowed inside folder with files extensions filters %#v user %#v",
|
||||
c.command, sshDestPath, c.connection.User.Username)
|
||||
return errUnsupportedConfig
|
||||
}
|
||||
}
|
||||
@@ -470,41 +484,34 @@ func (c *sshCommand) checkGitAllowed() error {
|
||||
|
||||
func (c *sshCommand) getSystemCommand() (systemCommand, error) {
|
||||
command := systemCommand{
|
||||
cmd: nil,
|
||||
realPath: "",
|
||||
cmd: nil,
|
||||
fsPath: "",
|
||||
quotaCheckPath: "",
|
||||
}
|
||||
args := make([]string, len(c.args))
|
||||
copy(args, c.args)
|
||||
var path string
|
||||
var fsPath, quotaPath string
|
||||
if len(c.args) > 0 {
|
||||
var err error
|
||||
sshPath := c.getDestPath()
|
||||
path, err = c.connection.fs.ResolvePath(sshPath)
|
||||
fsPath, err = c.connection.fs.ResolvePath(sshPath)
|
||||
if err != nil {
|
||||
return command, err
|
||||
}
|
||||
args = args[:len(args)-1]
|
||||
args = append(args, path)
|
||||
}
|
||||
if strings.HasPrefix(c.command, "git-") {
|
||||
// we don't allow git inside virtual folders or folders with files extensions filters
|
||||
if err := c.checkGitAllowed(); err != nil {
|
||||
return command, err
|
||||
quotaPath = sshPath
|
||||
fi, err := c.connection.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")
|
||||
}
|
||||
args = args[:len(args)-1]
|
||||
args = append(args, fsPath)
|
||||
}
|
||||
if err := c.isSystemCommandAllowed(); err != nil {
|
||||
return command, errUnsupportedConfig
|
||||
}
|
||||
if c.command == "rsync" {
|
||||
// if the user has virtual folders or file extensions filters we don't allow rsync since the rsync command
|
||||
// interacts with the filesystem directly and it is not aware about virtual folders/extensions files filters
|
||||
if len(c.connection.User.VirtualFolders) > 0 {
|
||||
c.connection.Log(logger.LevelDebug, logSenderSSH, "user %#v has virtual folders, rsync is not supported",
|
||||
c.connection.User.Username)
|
||||
return command, errUnsupportedConfig
|
||||
}
|
||||
if len(c.connection.User.Filters.FileExtensions) > 0 {
|
||||
c.connection.Log(logger.LevelDebug, logSenderSSH, "user %#v has file extensions filter, rsync is not supported",
|
||||
c.connection.User.Username)
|
||||
return command, errUnsupportedConfig
|
||||
}
|
||||
// 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
|
||||
@@ -521,38 +528,18 @@ func (c *sshCommand) getSystemCommand() (systemCommand, error) {
|
||||
}
|
||||
}
|
||||
}
|
||||
c.connection.Log(logger.LevelDebug, logSenderSSH, "new system command %#v, with args: %v path: %v", c.command, args, path)
|
||||
c.connection.Log(logger.LevelDebug, logSenderSSH, "new system command %#v, with args: %+v fs path %#v quota check path %#v",
|
||||
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.realPath = path
|
||||
command.fsPath = fsPath
|
||||
command.quotaCheckPath = quotaPath
|
||||
return command, nil
|
||||
}
|
||||
|
||||
func (c *sshCommand) rescanHomeDir() error {
|
||||
quotaTracking := dataprovider.GetQuotaTracking()
|
||||
if (!c.connection.User.HasQuotaRestrictions() && quotaTracking == 2) || quotaTracking == 0 {
|
||||
return nil
|
||||
}
|
||||
var err error
|
||||
var numFiles int
|
||||
var size int64
|
||||
if AddQuotaScan(c.connection.User.Username) {
|
||||
numFiles, size, err = c.connection.fs.ScanRootDirContents()
|
||||
if err != nil {
|
||||
c.connection.Log(logger.LevelWarn, logSenderSSH, "error scanning user home dir %#v: %v", c.connection.User.HomeDir, err)
|
||||
} else {
|
||||
err := dataprovider.UpdateUserQuota(dataProvider, c.connection.User, numFiles, size, true)
|
||||
c.connection.Log(logger.LevelDebug, logSenderSSH, "user home dir scanned, user: %#v, dir: %#v, error: %v",
|
||||
c.connection.User.Username, c.connection.User.HomeDir, err)
|
||||
}
|
||||
RemoveQuotaScan(c.connection.User.Username) //nolint:errcheck
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// for the supported commands, the destination path, if any, is the last argument
|
||||
func (c *sshCommand) getDestPath() string {
|
||||
if len(c.args) == 0 {
|
||||
@@ -642,6 +629,29 @@ func (c *sshCommand) checkCopyDestination(fsDestPath string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *sshCommand) getSizeForPath(name string) (int, int64, error) {
|
||||
if dataprovider.GetQuotaTracking() > 0 {
|
||||
fi, err := c.connection.fs.Lstat(name)
|
||||
if err != nil {
|
||||
if c.connection.fs.IsNotExist(err) {
|
||||
return 0, 0, nil
|
||||
}
|
||||
c.connection.Log(logger.LevelDebug, logSenderSSH, "unable to stat %#v error: %v", name, err)
|
||||
return 0, 0, err
|
||||
}
|
||||
if fi.IsDir() {
|
||||
files, size, err := c.connection.fs.GetDirSize(name)
|
||||
if err != nil {
|
||||
c.connection.Log(logger.LevelDebug, logSenderSSH, "unable to get size for dir %#v 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(), c.getMappedError(err))
|
||||
c.connection.channel.Write([]byte(errorString)) //nolint:errcheck
|
||||
|
||||
Reference in New Issue
Block a user