1package sftpd 2 3import ( 4 "crypto/md5" 5 "crypto/sha1" 6 "crypto/sha256" 7 "crypto/sha512" 8 "errors" 9 "fmt" 10 "hash" 11 "io" 12 "os" 13 "os/exec" 14 "path" 15 "runtime/debug" 16 "strings" 17 "sync" 18 19 "github.com/google/shlex" 20 fscopy "github.com/otiai10/copy" 21 "golang.org/x/crypto/ssh" 22 23 "github.com/drakkan/sftpgo/v2/common" 24 "github.com/drakkan/sftpgo/v2/dataprovider" 25 "github.com/drakkan/sftpgo/v2/logger" 26 "github.com/drakkan/sftpgo/v2/metric" 27 "github.com/drakkan/sftpgo/v2/sdk" 28 "github.com/drakkan/sftpgo/v2/util" 29 "github.com/drakkan/sftpgo/v2/vfs" 30) 31 32const ( 33 scpCmdName = "scp" 34 sshCommandLogSender = "SSHCommand" 35) 36 37var ( 38 errUnsupportedConfig = errors.New("command unsupported for this configuration") 39) 40 41type sshCommand struct { 42 command string 43 args []string 44 connection *Connection 45} 46 47type systemCommand struct { 48 cmd *exec.Cmd 49 fsPath string 50 quotaCheckPath string 51 fs vfs.Fs 52} 53 54func processSSHCommand(payload []byte, connection *Connection, enabledSSHCommands []string) bool { 55 var msg sshSubsystemExecMsg 56 if err := ssh.Unmarshal(payload, &msg); err == nil { 57 name, args, err := parseCommandPayload(msg.Command) 58 connection.Log(logger.LevelDebug, "new ssh command: %#v args: %v num args: %v user: %v, error: %v", 59 name, args, len(args), connection.User.Username, err) 60 if err == nil && util.IsStringInSlice(name, enabledSSHCommands) { 61 connection.command = msg.Command 62 if name == scpCmdName && len(args) >= 2 { 63 connection.SetProtocol(common.ProtocolSCP) 64 scpCommand := scpCommand{ 65 sshCommand: sshCommand{ 66 command: name, 67 connection: connection, 68 args: args}, 69 } 70 go scpCommand.handle() //nolint:errcheck 71 return true 72 } 73 if name != scpCmdName { 74 connection.SetProtocol(common.ProtocolSSH) 75 sshCommand := sshCommand{ 76 command: name, 77 connection: connection, 78 args: args, 79 } 80 go sshCommand.handle() //nolint:errcheck 81 return true 82 } 83 } else { 84 connection.Log(logger.LevelInfo, "ssh command not enabled/supported: %#v", name) 85 } 86 } 87 err := connection.CloseFS() 88 connection.Log(logger.LevelDebug, "unable to unmarshal ssh command, close fs, err: %v", err) 89 return false 90} 91 92func (c *sshCommand) handle() (err error) { 93 defer func() { 94 if r := recover(); r != nil { 95 logger.Error(logSender, "", "panic in handle ssh command: %#v stack strace: %v", r, string(debug.Stack())) 96 err = common.ErrGenericFailure 97 } 98 }() 99 common.Connections.Add(c.connection) 100 defer common.Connections.Remove(c.connection.GetID()) 101 102 c.connection.UpdateLastActivity() 103 if util.IsStringInSlice(c.command, sshHashCommands) { 104 return c.handleHashCommands() 105 } else if util.IsStringInSlice(c.command, systemCommands) { 106 command, err := c.getSystemCommand() 107 if err != nil { 108 return c.sendErrorResponse(err) 109 } 110 return c.executeSystemCommand(command) 111 } else if c.command == "cd" { 112 c.sendExitStatus(nil) 113 } else if c.command == "pwd" { 114 // hard coded response to "/" 115 c.connection.channel.Write([]byte("/\n")) //nolint:errcheck 116 c.sendExitStatus(nil) 117 } else if c.command == "sftpgo-copy" { 118 return c.handleSFTPGoCopy() 119 } else if c.command == "sftpgo-remove" { 120 return c.handleSFTPGoRemove() 121 } 122 return 123} 124 125func (c *sshCommand) handleSFTPGoCopy() error { 126 fsSrc, fsDst, sshSourcePath, sshDestPath, fsSourcePath, fsDestPath, err := c.getFsAndCopyPaths() 127 if err != nil { 128 return c.sendErrorResponse(err) 129 } 130 if !c.isLocalCopy(sshSourcePath, sshDestPath) { 131 return c.sendErrorResponse(errUnsupportedConfig) 132 } 133 134 if err := c.checkCopyDestination(fsDst, fsDestPath); err != nil { 135 return c.sendErrorResponse(c.connection.GetFsError(fsDst, err)) 136 } 137 138 c.connection.Log(logger.LevelDebug, "requested copy %#v -> %#v sftp paths %#v -> %#v", 139 fsSourcePath, fsDestPath, sshSourcePath, sshDestPath) 140 141 fi, err := fsSrc.Lstat(fsSourcePath) 142 if err != nil { 143 return c.sendErrorResponse(c.connection.GetFsError(fsSrc, err)) 144 } 145 if err := c.checkCopyPermissions(fsSrc, fsDst, fsSourcePath, fsDestPath, sshSourcePath, sshDestPath, fi); err != nil { 146 return c.sendErrorResponse(err) 147 } 148 filesNum := 0 149 filesSize := int64(0) 150 if fi.IsDir() { 151 filesNum, filesSize, err = fsSrc.GetDirSize(fsSourcePath) 152 if err != nil { 153 return c.sendErrorResponse(c.connection.GetFsError(fsSrc, err)) 154 } 155 if c.connection.User.HasVirtualFoldersInside(sshSourcePath) { 156 err := errors.New("unsupported copy source: the source directory contains virtual folders") 157 return c.sendErrorResponse(err) 158 } 159 if c.connection.User.HasVirtualFoldersInside(sshDestPath) { 160 err := errors.New("unsupported copy source: the destination directory contains virtual folders") 161 return c.sendErrorResponse(err) 162 } 163 } else if fi.Mode().IsRegular() { 164 if !c.connection.User.IsFileAllowed(sshDestPath) { 165 err := errors.New("unsupported copy destination: this file is not allowed") 166 return c.sendErrorResponse(err) 167 } 168 filesNum = 1 169 filesSize = fi.Size() 170 } else { 171 err := errors.New("unsupported copy source: only files and directories are supported") 172 return c.sendErrorResponse(err) 173 } 174 if err := c.checkCopyQuota(filesNum, filesSize, sshDestPath); err != nil { 175 return c.sendErrorResponse(err) 176 } 177 c.connection.Log(logger.LevelDebug, "start copy %#v -> %#v", fsSourcePath, fsDestPath) 178 err = fscopy.Copy(fsSourcePath, fsDestPath, fscopy.Options{ 179 OnSymlink: func(src string) fscopy.SymlinkAction { 180 return fscopy.Skip 181 }, 182 }) 183 if err != nil { 184 return c.sendErrorResponse(c.connection.GetFsError(fsSrc, err)) 185 } 186 c.updateQuota(sshDestPath, filesNum, filesSize) 187 c.connection.channel.Write([]byte("OK\n")) //nolint:errcheck 188 c.sendExitStatus(nil) 189 return nil 190} 191 192func (c *sshCommand) handleSFTPGoRemove() error { 193 sshDestPath, err := c.getRemovePath() 194 if err != nil { 195 return c.sendErrorResponse(err) 196 } 197 if !c.connection.User.HasPerm(dataprovider.PermDelete, path.Dir(sshDestPath)) { 198 return c.sendErrorResponse(common.ErrPermissionDenied) 199 } 200 fs, fsDestPath, err := c.connection.GetFsAndResolvedPath(sshDestPath) 201 if err != nil { 202 return c.sendErrorResponse(err) 203 } 204 if !vfs.IsLocalOrCryptoFs(fs) { 205 return c.sendErrorResponse(errUnsupportedConfig) 206 } 207 fi, err := fs.Lstat(fsDestPath) 208 if err != nil { 209 return c.sendErrorResponse(c.connection.GetFsError(fs, err)) 210 } 211 filesNum := 0 212 filesSize := int64(0) 213 if fi.IsDir() { 214 filesNum, filesSize, err = fs.GetDirSize(fsDestPath) 215 if err != nil { 216 return c.sendErrorResponse(c.connection.GetFsError(fs, err)) 217 } 218 if sshDestPath == "/" { 219 err := errors.New("removing root dir is not allowed") 220 return c.sendErrorResponse(err) 221 } 222 if c.connection.User.HasVirtualFoldersInside(sshDestPath) { 223 err := errors.New("unsupported remove source: this directory contains virtual folders") 224 return c.sendErrorResponse(err) 225 } 226 if c.connection.User.IsVirtualFolder(sshDestPath) { 227 err := errors.New("unsupported remove source: this directory is a virtual folder") 228 return c.sendErrorResponse(err) 229 } 230 } else if fi.Mode().IsRegular() { 231 filesNum = 1 232 filesSize = fi.Size() 233 } else { 234 err := errors.New("unsupported remove source: only files and directories are supported") 235 return c.sendErrorResponse(err) 236 } 237 238 err = os.RemoveAll(fsDestPath) 239 if err != nil { 240 return c.sendErrorResponse(err) 241 } 242 c.updateQuota(sshDestPath, -filesNum, -filesSize) 243 c.connection.channel.Write([]byte("OK\n")) //nolint:errcheck 244 c.sendExitStatus(nil) 245 return nil 246} 247 248func (c *sshCommand) updateQuota(sshDestPath string, filesNum int, filesSize int64) { 249 vfolder, err := c.connection.User.GetVirtualFolderForPath(sshDestPath) 250 if err == nil { 251 dataprovider.UpdateVirtualFolderQuota(&vfolder.BaseVirtualFolder, filesNum, filesSize, false) //nolint:errcheck 252 if vfolder.IsIncludedInUserQuota() { 253 dataprovider.UpdateUserQuota(&c.connection.User, filesNum, filesSize, false) //nolint:errcheck 254 } 255 } else { 256 dataprovider.UpdateUserQuota(&c.connection.User, filesNum, filesSize, false) //nolint:errcheck 257 } 258} 259 260func (c *sshCommand) handleHashCommands() error { 261 var h hash.Hash 262 if c.command == "md5sum" { 263 h = md5.New() 264 } else if c.command == "sha1sum" { 265 h = sha1.New() 266 } else if c.command == "sha256sum" { 267 h = sha256.New() 268 } else if c.command == "sha384sum" { 269 h = sha512.New384() 270 } else { 271 h = sha512.New() 272 } 273 var response string 274 if len(c.args) == 0 { 275 // without args we need to read the string to hash from stdin 276 buf := make([]byte, 4096) 277 n, err := c.connection.channel.Read(buf) 278 if err != nil && err != io.EOF { 279 return c.sendErrorResponse(err) 280 } 281 h.Write(buf[:n]) //nolint:errcheck 282 response = fmt.Sprintf("%x -\n", h.Sum(nil)) 283 } else { 284 sshPath := c.getDestPath() 285 if !c.connection.User.IsFileAllowed(sshPath) { 286 c.connection.Log(logger.LevelInfo, "hash not allowed for file %#v", sshPath) 287 return c.sendErrorResponse(c.connection.GetPermissionDeniedError()) 288 } 289 fs, fsPath, err := c.connection.GetFsAndResolvedPath(sshPath) 290 if err != nil { 291 return c.sendErrorResponse(err) 292 } 293 if !c.connection.User.HasPerm(dataprovider.PermListItems, sshPath) { 294 return c.sendErrorResponse(c.connection.GetPermissionDeniedError()) 295 } 296 hash, err := c.computeHashForFile(fs, h, fsPath) 297 if err != nil { 298 return c.sendErrorResponse(c.connection.GetFsError(fs, err)) 299 } 300 response = fmt.Sprintf("%v %v\n", hash, sshPath) 301 } 302 c.connection.channel.Write([]byte(response)) //nolint:errcheck 303 c.sendExitStatus(nil) 304 return nil 305} 306 307func (c *sshCommand) executeSystemCommand(command systemCommand) error { 308 sshDestPath := c.getDestPath() 309 if !c.isLocalPath(sshDestPath) { 310 return c.sendErrorResponse(errUnsupportedConfig) 311 } 312 quotaResult := c.connection.HasSpace(true, false, command.quotaCheckPath) 313 if !quotaResult.HasSpace { 314 return c.sendErrorResponse(common.ErrQuotaExceeded) 315 } 316 perms := []string{dataprovider.PermDownload, dataprovider.PermUpload, dataprovider.PermCreateDirs, dataprovider.PermListItems, 317 dataprovider.PermOverwrite, dataprovider.PermDelete} 318 if !c.connection.User.HasPerms(perms, sshDestPath) { 319 return c.sendErrorResponse(c.connection.GetPermissionDeniedError()) 320 } 321 322 initialFiles, initialSize, err := c.getSizeForPath(command.fs, command.fsPath) 323 if err != nil { 324 return c.sendErrorResponse(err) 325 } 326 327 stdin, err := command.cmd.StdinPipe() 328 if err != nil { 329 return c.sendErrorResponse(err) 330 } 331 stdout, err := command.cmd.StdoutPipe() 332 if err != nil { 333 return c.sendErrorResponse(err) 334 } 335 stderr, err := command.cmd.StderrPipe() 336 if err != nil { 337 return c.sendErrorResponse(err) 338 } 339 err = command.cmd.Start() 340 if err != nil { 341 return c.sendErrorResponse(err) 342 } 343 344 closeCmdOnError := func() { 345 c.connection.Log(logger.LevelDebug, "kill cmd: %#v and close ssh channel after read or write error", 346 c.connection.command) 347 killerr := command.cmd.Process.Kill() 348 closerr := c.connection.channel.Close() 349 c.connection.Log(logger.LevelDebug, "kill cmd error: %v close channel error: %v", killerr, closerr) 350 } 351 var once sync.Once 352 commandResponse := make(chan bool) 353 354 remainingQuotaSize := quotaResult.GetRemainingSize() 355 356 go func() { 357 defer stdin.Close() 358 baseTransfer := common.NewBaseTransfer(nil, c.connection.BaseConnection, nil, command.fsPath, command.fsPath, sshDestPath, 359 common.TransferUpload, 0, 0, remainingQuotaSize, false, command.fs) 360 transfer := newTransfer(baseTransfer, nil, nil, nil) 361 362 w, e := transfer.copyFromReaderToWriter(stdin, c.connection.channel) 363 c.connection.Log(logger.LevelDebug, "command: %#v, copy from remote command to sdtin ended, written: %v, "+ 364 "initial remaining quota: %v, err: %v", c.connection.command, w, remainingQuotaSize, e) 365 if e != nil { 366 once.Do(closeCmdOnError) 367 } 368 }() 369 370 go func() { 371 baseTransfer := common.NewBaseTransfer(nil, c.connection.BaseConnection, nil, command.fsPath, command.fsPath, sshDestPath, 372 common.TransferDownload, 0, 0, 0, false, command.fs) 373 transfer := newTransfer(baseTransfer, nil, nil, nil) 374 375 w, e := transfer.copyFromReaderToWriter(c.connection.channel, stdout) 376 c.connection.Log(logger.LevelDebug, "command: %#v, copy from sdtout to remote command ended, written: %v err: %v", 377 c.connection.command, w, e) 378 if e != nil { 379 once.Do(closeCmdOnError) 380 } 381 commandResponse <- true 382 }() 383 384 go func() { 385 baseTransfer := common.NewBaseTransfer(nil, c.connection.BaseConnection, nil, command.fsPath, command.fsPath, sshDestPath, 386 common.TransferDownload, 0, 0, 0, false, command.fs) 387 transfer := newTransfer(baseTransfer, nil, nil, nil) 388 389 w, e := transfer.copyFromReaderToWriter(c.connection.channel.(ssh.Channel).Stderr(), stderr) 390 c.connection.Log(logger.LevelDebug, "command: %#v, copy from sdterr to remote command ended, written: %v err: %v", 391 c.connection.command, w, e) 392 // os.ErrClosed means that the command is finished so we don't need to do anything 393 if (e != nil && !errors.Is(e, os.ErrClosed)) || w > 0 { 394 once.Do(closeCmdOnError) 395 } 396 }() 397 398 <-commandResponse 399 err = command.cmd.Wait() 400 c.sendExitStatus(err) 401 402 numFiles, dirSize, errSize := c.getSizeForPath(command.fs, command.fsPath) 403 if errSize == nil { 404 c.updateQuota(sshDestPath, numFiles-initialFiles, dirSize-initialSize) 405 } 406 c.connection.Log(logger.LevelDebug, "command %#v finished for path %#v, initial files %v initial size %v "+ 407 "current files %v current size %v size err: %v", c.connection.command, command.fsPath, initialFiles, initialSize, 408 numFiles, dirSize, errSize) 409 return c.connection.GetFsError(command.fs, err) 410} 411 412func (c *sshCommand) isSystemCommandAllowed() error { 413 sshDestPath := c.getDestPath() 414 if c.connection.User.IsVirtualFolder(sshDestPath) { 415 // overlapped virtual path are not allowed 416 return nil 417 } 418 if c.connection.User.HasVirtualFoldersInside(sshDestPath) { 419 c.connection.Log(logger.LevelDebug, "command %#v is not allowed, path %#v has virtual folders inside it, user %#v", 420 c.command, sshDestPath, c.connection.User.Username) 421 return errUnsupportedConfig 422 } 423 for _, f := range c.connection.User.Filters.FilePatterns { 424 if f.Path == sshDestPath { 425 c.connection.Log(logger.LevelDebug, 426 "command %#v is not allowed inside folders with file patterns filters %#v user %#v", 427 c.command, sshDestPath, c.connection.User.Username) 428 return errUnsupportedConfig 429 } 430 if len(sshDestPath) > len(f.Path) { 431 if strings.HasPrefix(sshDestPath, f.Path+"/") || f.Path == "/" { 432 c.connection.Log(logger.LevelDebug, 433 "command %#v is not allowed it includes folders with file patterns filters %#v user %#v", 434 c.command, sshDestPath, c.connection.User.Username) 435 return errUnsupportedConfig 436 } 437 } 438 if len(sshDestPath) < len(f.Path) { 439 if strings.HasPrefix(sshDestPath+"/", f.Path) || sshDestPath == "/" { 440 c.connection.Log(logger.LevelDebug, 441 "command %#v is not allowed inside folder with file patterns filters %#v user %#v", 442 c.command, sshDestPath, c.connection.User.Username) 443 return errUnsupportedConfig 444 } 445 } 446 } 447 return nil 448} 449 450func (c *sshCommand) getSystemCommand() (systemCommand, error) { 451 command := systemCommand{ 452 cmd: nil, 453 fs: nil, 454 fsPath: "", 455 quotaCheckPath: "", 456 } 457 args := make([]string, len(c.args)) 458 copy(args, c.args) 459 var fsPath, quotaPath string 460 sshPath := c.getDestPath() 461 fs, err := c.connection.User.GetFilesystemForPath(sshPath, c.connection.ID) 462 if err != nil { 463 return command, err 464 } 465 if len(c.args) > 0 { 466 var err error 467 fsPath, err = fs.ResolvePath(sshPath) 468 if err != nil { 469 return command, c.connection.GetFsError(fs, err) 470 } 471 quotaPath = sshPath 472 fi, err := fs.Stat(fsPath) 473 if err == nil && fi.IsDir() { 474 // if the target is an existing dir the command will write inside this dir 475 // so we need to check the quota for this directory and not its parent dir 476 quotaPath = path.Join(sshPath, "fakecontent") 477 } 478 if strings.HasSuffix(sshPath, "/") && !strings.HasSuffix(fsPath, string(os.PathSeparator)) { 479 fsPath += string(os.PathSeparator) 480 c.connection.Log(logger.LevelDebug, "path separator added to fsPath %#v", fsPath) 481 } 482 args = args[:len(args)-1] 483 args = append(args, fsPath) 484 } 485 if err := c.isSystemCommandAllowed(); err != nil { 486 return command, errUnsupportedConfig 487 } 488 if c.command == "rsync" { 489 // we cannot avoid that rsync creates symlinks so if the user has the permission 490 // to create symlinks we add the option --safe-links to the received rsync command if 491 // it is not already set. This should prevent to create symlinks that point outside 492 // the home dir. 493 // If the user cannot create symlinks we add the option --munge-links, if it is not 494 // already set. This should make symlinks unusable (but manually recoverable) 495 if c.connection.User.HasPerm(dataprovider.PermCreateSymlinks, c.getDestPath()) { 496 if !util.IsStringInSlice("--safe-links", args) { 497 args = append([]string{"--safe-links"}, args...) 498 } 499 } else { 500 if !util.IsStringInSlice("--munge-links", args) { 501 args = append([]string{"--munge-links"}, args...) 502 } 503 } 504 } 505 c.connection.Log(logger.LevelDebug, "new system command %#v, with args: %+v fs path %#v quota check path %#v", 506 c.command, args, fsPath, quotaPath) 507 cmd := exec.Command(c.command, args...) 508 uid := c.connection.User.GetUID() 509 gid := c.connection.User.GetGID() 510 cmd = wrapCmd(cmd, uid, gid) 511 command.cmd = cmd 512 command.fsPath = fsPath 513 command.quotaCheckPath = quotaPath 514 command.fs = fs 515 return command, nil 516} 517 518// for the supported commands, the destination path, if any, is the last argument 519func (c *sshCommand) getDestPath() string { 520 if len(c.args) == 0 { 521 return "" 522 } 523 return cleanCommandPath(c.args[len(c.args)-1]) 524} 525 526// for the supported commands, the destination path, if any, is the second-last argument 527func (c *sshCommand) getSourcePath() string { 528 if len(c.args) < 2 { 529 return "" 530 } 531 return cleanCommandPath(c.args[len(c.args)-2]) 532} 533 534func cleanCommandPath(name string) string { 535 name = strings.Trim(name, "'") 536 name = strings.Trim(name, "\"") 537 result := util.CleanPath(name) 538 if strings.HasSuffix(name, "/") && !strings.HasSuffix(result, "/") { 539 result += "/" 540 } 541 return result 542} 543 544func (c *sshCommand) getFsAndCopyPaths() (vfs.Fs, vfs.Fs, string, string, string, string, error) { 545 sshSourcePath := strings.TrimSuffix(c.getSourcePath(), "/") 546 sshDestPath := c.getDestPath() 547 if strings.HasSuffix(sshDestPath, "/") { 548 sshDestPath = path.Join(sshDestPath, path.Base(sshSourcePath)) 549 } 550 if sshSourcePath == "" || sshDestPath == "" || len(c.args) != 2 { 551 err := errors.New("usage sftpgo-copy <source dir path> <destination dir path>") 552 return nil, nil, "", "", "", "", err 553 } 554 fsSrc, fsSourcePath, err := c.connection.GetFsAndResolvedPath(sshSourcePath) 555 if err != nil { 556 return nil, nil, "", "", "", "", err 557 } 558 fsDst, fsDestPath, err := c.connection.GetFsAndResolvedPath(sshDestPath) 559 if err != nil { 560 return nil, nil, "", "", "", "", err 561 } 562 return fsSrc, fsDst, sshSourcePath, sshDestPath, fsSourcePath, fsDestPath, nil 563} 564 565func (c *sshCommand) hasCopyPermissions(sshSourcePath, sshDestPath string, srcInfo os.FileInfo) bool { 566 if !c.connection.User.HasPerm(dataprovider.PermListItems, path.Dir(sshSourcePath)) { 567 return false 568 } 569 if srcInfo.IsDir() { 570 return c.connection.User.HasPerm(dataprovider.PermCreateDirs, path.Dir(sshDestPath)) 571 } else if srcInfo.Mode()&os.ModeSymlink != 0 { 572 return c.connection.User.HasPerm(dataprovider.PermCreateSymlinks, path.Dir(sshDestPath)) 573 } 574 return c.connection.User.HasPerm(dataprovider.PermUpload, path.Dir(sshDestPath)) 575} 576 577// fsSourcePath must be a directory 578func (c *sshCommand) checkRecursiveCopyPermissions(fsSrc vfs.Fs, fsDst vfs.Fs, fsSourcePath, fsDestPath, sshDestPath string) error { 579 if !c.connection.User.HasPerm(dataprovider.PermCreateDirs, path.Dir(sshDestPath)) { 580 return common.ErrPermissionDenied 581 } 582 dstPerms := []string{ 583 dataprovider.PermCreateDirs, 584 dataprovider.PermCreateSymlinks, 585 dataprovider.PermUpload, 586 } 587 588 err := fsSrc.Walk(fsSourcePath, func(walkedPath string, info os.FileInfo, err error) error { 589 if err != nil { 590 return c.connection.GetFsError(fsSrc, err) 591 } 592 fsDstSubPath := strings.Replace(walkedPath, fsSourcePath, fsDestPath, 1) 593 sshSrcSubPath := fsSrc.GetRelativePath(walkedPath) 594 sshDstSubPath := fsDst.GetRelativePath(fsDstSubPath) 595 // If the current dir has no subdirs with defined permissions inside it 596 // and it has all the possible permissions we can stop scanning 597 if !c.connection.User.HasPermissionsInside(path.Dir(sshSrcSubPath)) && 598 !c.connection.User.HasPermissionsInside(path.Dir(sshDstSubPath)) { 599 if c.connection.User.HasPerm(dataprovider.PermListItems, path.Dir(sshSrcSubPath)) && 600 c.connection.User.HasPerms(dstPerms, path.Dir(sshDstSubPath)) { 601 return common.ErrSkipPermissionsCheck 602 } 603 } 604 if !c.hasCopyPermissions(sshSrcSubPath, sshDstSubPath, info) { 605 return common.ErrPermissionDenied 606 } 607 return nil 608 }) 609 if err == common.ErrSkipPermissionsCheck { 610 err = nil 611 } 612 return err 613} 614 615func (c *sshCommand) checkCopyPermissions(fsSrc vfs.Fs, fsDst vfs.Fs, fsSourcePath, fsDestPath, sshSourcePath, sshDestPath string, info os.FileInfo) error { 616 if info.IsDir() { 617 return c.checkRecursiveCopyPermissions(fsSrc, fsDst, fsSourcePath, fsDestPath, sshDestPath) 618 } 619 if !c.hasCopyPermissions(sshSourcePath, sshDestPath, info) { 620 return c.connection.GetPermissionDeniedError() 621 } 622 return nil 623} 624 625func (c *sshCommand) getRemovePath() (string, error) { 626 sshDestPath := c.getDestPath() 627 if sshDestPath == "" || len(c.args) != 1 { 628 err := errors.New("usage sftpgo-remove <destination path>") 629 return "", err 630 } 631 if len(sshDestPath) > 1 { 632 sshDestPath = strings.TrimSuffix(sshDestPath, "/") 633 } 634 return sshDestPath, nil 635} 636 637func (c *sshCommand) isLocalPath(virtualPath string) bool { 638 folder, err := c.connection.User.GetVirtualFolderForPath(virtualPath) 639 if err != nil { 640 return c.connection.User.FsConfig.Provider == sdk.LocalFilesystemProvider 641 } 642 return folder.FsConfig.Provider == sdk.LocalFilesystemProvider 643} 644 645func (c *sshCommand) isLocalCopy(virtualSourcePath, virtualTargetPath string) bool { 646 if !c.isLocalPath(virtualSourcePath) { 647 return false 648 } 649 650 return c.isLocalPath(virtualTargetPath) 651} 652 653func (c *sshCommand) checkCopyDestination(fs vfs.Fs, fsDestPath string) error { 654 _, err := fs.Lstat(fsDestPath) 655 if err == nil { 656 err := errors.New("invalid copy destination: cannot overwrite an existing file or directory") 657 return err 658 } else if !fs.IsNotExist(err) { 659 return err 660 } 661 return nil 662} 663 664func (c *sshCommand) checkCopyQuota(numFiles int, filesSize int64, requestPath string) error { 665 quotaResult := c.connection.HasSpace(true, false, requestPath) 666 if !quotaResult.HasSpace { 667 return common.ErrQuotaExceeded 668 } 669 if quotaResult.QuotaFiles > 0 { 670 remainingFiles := quotaResult.GetRemainingFiles() 671 if remainingFiles < numFiles { 672 c.connection.Log(logger.LevelDebug, "copy not allowed, file limit will be exceeded, "+ 673 "remaining files: %v to copy: %v", remainingFiles, numFiles) 674 return common.ErrQuotaExceeded 675 } 676 } 677 if quotaResult.QuotaSize > 0 { 678 remainingSize := quotaResult.GetRemainingSize() 679 if remainingSize < filesSize { 680 c.connection.Log(logger.LevelDebug, "copy not allowed, size limit will be exceeded, "+ 681 "remaining size: %v to copy: %v", remainingSize, filesSize) 682 return common.ErrQuotaExceeded 683 } 684 } 685 return nil 686} 687 688func (c *sshCommand) getSizeForPath(fs vfs.Fs, name string) (int, int64, error) { 689 if dataprovider.GetQuotaTracking() > 0 { 690 fi, err := fs.Lstat(name) 691 if err != nil { 692 if fs.IsNotExist(err) { 693 return 0, 0, nil 694 } 695 c.connection.Log(logger.LevelDebug, "unable to stat %#v error: %v", name, err) 696 return 0, 0, err 697 } 698 if fi.IsDir() { 699 files, size, err := fs.GetDirSize(name) 700 if err != nil { 701 c.connection.Log(logger.LevelDebug, "unable to get size for dir %#v error: %v", name, err) 702 } 703 return files, size, err 704 } else if fi.Mode().IsRegular() { 705 return 1, fi.Size(), nil 706 } 707 } 708 return 0, 0, nil 709} 710 711func (c *sshCommand) sendErrorResponse(err error) error { 712 errorString := fmt.Sprintf("%v: %v %v\n", c.command, c.getDestPath(), err) 713 c.connection.channel.Write([]byte(errorString)) //nolint:errcheck 714 c.sendExitStatus(err) 715 return err 716} 717 718func (c *sshCommand) sendExitStatus(err error) { 719 status := uint32(0) 720 vCmdPath := c.getDestPath() 721 cmdPath := "" 722 targetPath := "" 723 vTargetPath := "" 724 if c.command == "sftpgo-copy" { 725 vTargetPath = vCmdPath 726 vCmdPath = c.getSourcePath() 727 } 728 if err != nil { 729 status = uint32(1) 730 c.connection.Log(logger.LevelWarn, "command failed: %#v args: %v user: %v err: %v", 731 c.command, c.args, c.connection.User.Username, err) 732 } 733 exitStatus := sshSubsystemExitStatus{ 734 Status: status, 735 } 736 _, errClose := c.connection.channel.(ssh.Channel).SendRequest("exit-status", false, ssh.Marshal(&exitStatus)) 737 c.connection.Log(logger.LevelDebug, "exit status sent, error: %v", errClose) 738 c.connection.channel.Close() 739 // for scp we notify single uploads/downloads 740 if c.command != scpCmdName { 741 metric.SSHCommandCompleted(err) 742 if vCmdPath != "" { 743 _, p, errFs := c.connection.GetFsAndResolvedPath(vCmdPath) 744 if errFs == nil { 745 cmdPath = p 746 } 747 } 748 if vTargetPath != "" { 749 _, p, errFs := c.connection.GetFsAndResolvedPath(vTargetPath) 750 if errFs == nil { 751 targetPath = p 752 } 753 } 754 common.ExecuteActionNotification(&c.connection.User, common.OperationSSHCmd, cmdPath, vCmdPath, targetPath, 755 vTargetPath, c.command, common.ProtocolSSH, c.connection.GetRemoteIP(), 0, err) 756 if err == nil { 757 logger.CommandLog(sshCommandLogSender, cmdPath, targetPath, c.connection.User.Username, "", c.connection.ID, 758 common.ProtocolSSH, -1, -1, "", "", c.connection.command, -1, c.connection.GetLocalAddress(), 759 c.connection.GetRemoteAddress()) 760 } 761 } 762} 763 764func (c *sshCommand) computeHashForFile(fs vfs.Fs, hasher hash.Hash, path string) (string, error) { 765 hash := "" 766 f, r, _, err := fs.Open(path, 0) 767 if err != nil { 768 return hash, err 769 } 770 var reader io.ReadCloser 771 if f != nil { 772 reader = f 773 } else { 774 reader = r 775 } 776 defer reader.Close() 777 _, err = io.Copy(hasher, reader) 778 if err == nil { 779 hash = fmt.Sprintf("%x", hasher.Sum(nil)) 780 } 781 return hash, err 782} 783 784func parseCommandPayload(command string) (string, []string, error) { 785 parts, err := shlex.Split(command) 786 if err == nil && len(parts) == 0 { 787 err = fmt.Errorf("invalid command: %#v", command) 788 } 789 if err != nil { 790 return "", []string{}, err 791 } 792 if len(parts) < 2 { 793 return parts[0], []string{}, nil 794 } 795 return parts[0], parts[1:], nil 796} 797