1
0

ssh_cmd.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502
  1. package sftpd
  2. import (
  3. "crypto/md5"
  4. "crypto/sha1"
  5. "crypto/sha256"
  6. "crypto/sha512"
  7. "errors"
  8. "fmt"
  9. "hash"
  10. "io"
  11. "os"
  12. "os/exec"
  13. "strings"
  14. "sync"
  15. "time"
  16. "github.com/google/shlex"
  17. "golang.org/x/crypto/ssh"
  18. "github.com/drakkan/sftpgo/dataprovider"
  19. "github.com/drakkan/sftpgo/logger"
  20. "github.com/drakkan/sftpgo/metrics"
  21. "github.com/drakkan/sftpgo/utils"
  22. "github.com/drakkan/sftpgo/vfs"
  23. )
  24. const scpCmdName = "scp"
  25. var (
  26. errQuotaExceeded = errors.New("denying write due to space limit")
  27. errPermissionDenied = errors.New("Permission denied. You don't have the permissions to execute this command")
  28. errUnsupportedConfig = errors.New("command unsupported for this configuration")
  29. )
  30. type sshCommand struct {
  31. command string
  32. args []string
  33. connection Connection
  34. }
  35. type systemCommand struct {
  36. cmd *exec.Cmd
  37. realPath string
  38. }
  39. func processSSHCommand(payload []byte, connection *Connection, channel ssh.Channel, enabledSSHCommands []string) bool {
  40. var msg sshSubsystemExecMsg
  41. if err := ssh.Unmarshal(payload, &msg); err == nil {
  42. name, args, err := parseCommandPayload(msg.Command)
  43. connection.Log(logger.LevelDebug, logSenderSSH, "new ssh command: %#v args: %v num args: %v user: %v, error: %v",
  44. name, args, len(args), connection.User.Username, err)
  45. if err == nil && utils.IsStringInSlice(name, enabledSSHCommands) {
  46. connection.command = msg.Command
  47. if name == scpCmdName && len(args) >= 2 {
  48. connection.protocol = protocolSCP
  49. connection.channel = channel
  50. scpCommand := scpCommand{
  51. sshCommand: sshCommand{
  52. command: name,
  53. connection: *connection,
  54. args: args},
  55. }
  56. go scpCommand.handle() //nolint:errcheck
  57. return true
  58. }
  59. if name != scpCmdName {
  60. connection.protocol = protocolSSH
  61. connection.channel = channel
  62. sshCommand := sshCommand{
  63. command: name,
  64. connection: *connection,
  65. args: args,
  66. }
  67. go sshCommand.handle() //nolint:errcheck
  68. return true
  69. }
  70. } else {
  71. connection.Log(logger.LevelInfo, logSenderSSH, "ssh command not enabled/supported: %#v", name)
  72. }
  73. }
  74. return false
  75. }
  76. func (c *sshCommand) handle() error {
  77. addConnection(c.connection)
  78. defer removeConnection(c.connection)
  79. updateConnectionActivity(c.connection.ID)
  80. if utils.IsStringInSlice(c.command, sshHashCommands) {
  81. return c.handleHashCommands()
  82. } else if utils.IsStringInSlice(c.command, systemCommands) {
  83. command, err := c.getSystemCommand()
  84. if err != nil {
  85. return c.sendErrorResponse(err)
  86. }
  87. return c.executeSystemCommand(command)
  88. } else if c.command == "cd" {
  89. c.sendExitStatus(nil)
  90. } else if c.command == "pwd" {
  91. // hard coded response to "/"
  92. c.connection.channel.Write([]byte("/\n")) //nolint:errcheck
  93. c.sendExitStatus(nil)
  94. }
  95. return nil
  96. }
  97. func (c *sshCommand) handleHashCommands() error {
  98. if !vfs.IsLocalOsFs(c.connection.fs) {
  99. return c.sendErrorResponse(errUnsupportedConfig)
  100. }
  101. var h hash.Hash
  102. if c.command == "md5sum" {
  103. h = md5.New()
  104. } else if c.command == "sha1sum" {
  105. h = sha1.New()
  106. } else if c.command == "sha256sum" {
  107. h = sha256.New()
  108. } else if c.command == "sha384sum" {
  109. h = sha512.New384()
  110. } else {
  111. h = sha512.New()
  112. }
  113. var response string
  114. if len(c.args) == 0 {
  115. // without args we need to read the string to hash from stdin
  116. buf := make([]byte, 4096)
  117. n, err := c.connection.channel.Read(buf)
  118. if err != nil && err != io.EOF {
  119. return c.sendErrorResponse(err)
  120. }
  121. h.Write(buf[:n]) //nolint:errcheck
  122. response = fmt.Sprintf("%x -\n", h.Sum(nil))
  123. } else {
  124. sshPath := c.getDestPath()
  125. if !c.connection.User.IsFileAllowed(sshPath) {
  126. c.connection.Log(logger.LevelInfo, logSenderSSH, "hash not allowed for file %#v", sshPath)
  127. return c.sendErrorResponse(errPermissionDenied)
  128. }
  129. fsPath, err := c.connection.fs.ResolvePath(sshPath)
  130. if err != nil {
  131. return c.sendErrorResponse(err)
  132. }
  133. if !c.connection.User.HasPerm(dataprovider.PermListItems, sshPath) {
  134. return c.sendErrorResponse(errPermissionDenied)
  135. }
  136. hash, err := computeHashForFile(h, fsPath)
  137. if err != nil {
  138. return c.sendErrorResponse(err)
  139. }
  140. response = fmt.Sprintf("%v %v\n", hash, sshPath)
  141. }
  142. c.connection.channel.Write([]byte(response)) //nolint:errcheck
  143. c.sendExitStatus(nil)
  144. return nil
  145. }
  146. func (c *sshCommand) executeSystemCommand(command systemCommand) error {
  147. if !vfs.IsLocalOsFs(c.connection.fs) {
  148. return c.sendErrorResponse(errUnsupportedConfig)
  149. }
  150. if c.connection.User.QuotaFiles > 0 && c.connection.User.UsedQuotaFiles > c.connection.User.QuotaFiles {
  151. return c.sendErrorResponse(errQuotaExceeded)
  152. }
  153. perms := []string{dataprovider.PermDownload, dataprovider.PermUpload, dataprovider.PermCreateDirs, dataprovider.PermListItems,
  154. dataprovider.PermOverwrite, dataprovider.PermDelete, dataprovider.PermRename}
  155. if !c.connection.User.HasPerms(perms, c.getDestPath()) {
  156. return c.sendErrorResponse(errPermissionDenied)
  157. }
  158. stdin, err := command.cmd.StdinPipe()
  159. if err != nil {
  160. return c.sendErrorResponse(err)
  161. }
  162. stdout, err := command.cmd.StdoutPipe()
  163. if err != nil {
  164. return c.sendErrorResponse(err)
  165. }
  166. stderr, err := command.cmd.StderrPipe()
  167. if err != nil {
  168. return c.sendErrorResponse(err)
  169. }
  170. err = command.cmd.Start()
  171. if err != nil {
  172. return c.sendErrorResponse(err)
  173. }
  174. closeCmdOnError := func() {
  175. c.connection.Log(logger.LevelDebug, logSenderSSH, "kill cmd: %#v and close ssh channel after read or write error",
  176. c.connection.command)
  177. killerr := command.cmd.Process.Kill()
  178. closerr := c.connection.channel.Close()
  179. c.connection.Log(logger.LevelDebug, logSenderSSH, "kill cmd error: %v close channel error: %v", killerr, closerr)
  180. }
  181. var once sync.Once
  182. commandResponse := make(chan bool)
  183. go func() {
  184. defer stdin.Close()
  185. remainingQuotaSize := int64(0)
  186. if c.connection.User.QuotaSize > 0 {
  187. remainingQuotaSize = c.connection.User.QuotaSize - c.connection.User.UsedQuotaSize
  188. }
  189. transfer := Transfer{
  190. file: nil,
  191. path: command.realPath,
  192. start: time.Now(),
  193. bytesSent: 0,
  194. bytesReceived: 0,
  195. user: c.connection.User,
  196. connectionID: c.connection.ID,
  197. transferType: transferUpload,
  198. lastActivity: time.Now(),
  199. isNewFile: false,
  200. protocol: c.connection.protocol,
  201. transferError: nil,
  202. isFinished: false,
  203. minWriteOffset: 0,
  204. lock: new(sync.Mutex),
  205. }
  206. addTransfer(&transfer)
  207. defer removeTransfer(&transfer) //nolint:errcheck
  208. w, e := transfer.copyFromReaderToWriter(stdin, c.connection.channel, remainingQuotaSize)
  209. c.connection.Log(logger.LevelDebug, logSenderSSH, "command: %#v, copy from remote command to sdtin ended, written: %v, "+
  210. "initial remaining quota: %v, err: %v", c.connection.command, w, remainingQuotaSize, e)
  211. if e != nil {
  212. once.Do(closeCmdOnError)
  213. }
  214. }()
  215. go func() {
  216. transfer := Transfer{
  217. file: nil,
  218. path: command.realPath,
  219. start: time.Now(),
  220. bytesSent: 0,
  221. bytesReceived: 0,
  222. user: c.connection.User,
  223. connectionID: c.connection.ID,
  224. transferType: transferDownload,
  225. lastActivity: time.Now(),
  226. isNewFile: false,
  227. protocol: c.connection.protocol,
  228. transferError: nil,
  229. isFinished: false,
  230. minWriteOffset: 0,
  231. lock: new(sync.Mutex),
  232. }
  233. addTransfer(&transfer)
  234. defer removeTransfer(&transfer) //nolint:errcheck
  235. w, e := transfer.copyFromReaderToWriter(c.connection.channel, stdout, 0)
  236. c.connection.Log(logger.LevelDebug, logSenderSSH, "command: %#v, copy from sdtout to remote command ended, written: %v err: %v",
  237. c.connection.command, w, e)
  238. if e != nil {
  239. once.Do(closeCmdOnError)
  240. }
  241. commandResponse <- true
  242. }()
  243. go func() {
  244. transfer := Transfer{
  245. file: nil,
  246. path: command.realPath,
  247. start: time.Now(),
  248. bytesSent: 0,
  249. bytesReceived: 0,
  250. user: c.connection.User,
  251. connectionID: c.connection.ID,
  252. transferType: transferDownload,
  253. lastActivity: time.Now(),
  254. isNewFile: false,
  255. protocol: c.connection.protocol,
  256. transferError: nil,
  257. isFinished: false,
  258. minWriteOffset: 0,
  259. lock: new(sync.Mutex),
  260. }
  261. addTransfer(&transfer)
  262. defer removeTransfer(&transfer) //nolint:errcheck
  263. w, e := transfer.copyFromReaderToWriter(c.connection.channel.Stderr(), stderr, 0)
  264. c.connection.Log(logger.LevelDebug, logSenderSSH, "command: %#v, copy from sdterr to remote command ended, written: %v err: %v",
  265. c.connection.command, w, e)
  266. // os.ErrClosed means that the command is finished so we don't need to do anything
  267. if (e != nil && !errors.Is(e, os.ErrClosed)) || w > 0 {
  268. once.Do(closeCmdOnError)
  269. }
  270. }()
  271. <-commandResponse
  272. err = command.cmd.Wait()
  273. c.sendExitStatus(err)
  274. c.rescanHomeDir() //nolint:errcheck
  275. return err
  276. }
  277. func (c *sshCommand) checkGitAllowed() error {
  278. gitPath := c.getDestPath()
  279. for _, v := range c.connection.User.VirtualFolders {
  280. if v.VirtualPath == gitPath {
  281. c.connection.Log(logger.LevelDebug, logSenderSSH, "git is not supported inside virtual folder %#v user %#v",
  282. gitPath, c.connection.User.Username)
  283. return errUnsupportedConfig
  284. }
  285. if len(gitPath) > len(v.VirtualPath) {
  286. if strings.HasPrefix(gitPath, v.VirtualPath+"/") {
  287. c.connection.Log(logger.LevelDebug, logSenderSSH, "git is not supported inside virtual folder %#v user %#v",
  288. gitPath, c.connection.User.Username)
  289. return errUnsupportedConfig
  290. }
  291. }
  292. }
  293. for _, f := range c.connection.User.Filters.FileExtensions {
  294. if f.Path == gitPath {
  295. c.connection.Log(logger.LevelDebug, logSenderSSH,
  296. "git is not supported inside folder with files extensions filters %#v user %#v", gitPath,
  297. c.connection.User.Username)
  298. return errUnsupportedConfig
  299. }
  300. if len(gitPath) > len(f.Path) {
  301. if strings.HasPrefix(gitPath, f.Path+"/") || f.Path == "/" {
  302. c.connection.Log(logger.LevelDebug, logSenderSSH,
  303. "git is not supported inside folder with files extensions filters %#v user %#v", gitPath,
  304. c.connection.User.Username)
  305. return errUnsupportedConfig
  306. }
  307. }
  308. }
  309. return nil
  310. }
  311. func (c *sshCommand) getSystemCommand() (systemCommand, error) {
  312. command := systemCommand{
  313. cmd: nil,
  314. realPath: "",
  315. }
  316. args := make([]string, len(c.args))
  317. copy(args, c.args)
  318. var path string
  319. if len(c.args) > 0 {
  320. var err error
  321. sshPath := c.getDestPath()
  322. path, err = c.connection.fs.ResolvePath(sshPath)
  323. if err != nil {
  324. return command, err
  325. }
  326. args = args[:len(args)-1]
  327. args = append(args, path)
  328. }
  329. if strings.HasPrefix(c.command, "git-") {
  330. // we don't allow git inside virtual folders or folders with files extensions filters
  331. if err := c.checkGitAllowed(); err != nil {
  332. return command, err
  333. }
  334. }
  335. if c.command == "rsync" {
  336. // if the user has virtual folders or file extensions filters we don't allow rsync since the rsync command
  337. // interacts with the filesystem directly and it is not aware about virtual folders/extensions files filters
  338. if len(c.connection.User.VirtualFolders) > 0 {
  339. c.connection.Log(logger.LevelDebug, logSenderSSH, "user %#v has virtual folders, rsync is not supported",
  340. c.connection.User.Username)
  341. return command, errUnsupportedConfig
  342. }
  343. if len(c.connection.User.Filters.FileExtensions) > 0 {
  344. c.connection.Log(logger.LevelDebug, logSenderSSH, "user %#v has file extensions filter, rsync is not supported",
  345. c.connection.User.Username)
  346. return command, errUnsupportedConfig
  347. }
  348. // we cannot avoid that rsync creates symlinks so if the user has the permission
  349. // to create symlinks we add the option --safe-links to the received rsync command if
  350. // it is not already set. This should prevent to create symlinks that point outside
  351. // the home dir.
  352. // If the user cannot create symlinks we add the option --munge-links, if it is not
  353. // already set. This should make symlinks unusable (but manually recoverable)
  354. if c.connection.User.HasPerm(dataprovider.PermCreateSymlinks, c.getDestPath()) {
  355. if !utils.IsStringInSlice("--safe-links", args) {
  356. args = append([]string{"--safe-links"}, args...)
  357. }
  358. } else {
  359. if !utils.IsStringInSlice("--munge-links", args) {
  360. args = append([]string{"--munge-links"}, args...)
  361. }
  362. }
  363. }
  364. c.connection.Log(logger.LevelDebug, logSenderSSH, "new system command %#v, with args: %v path: %v", c.command, args, path)
  365. cmd := exec.Command(c.command, args...)
  366. uid := c.connection.User.GetUID()
  367. gid := c.connection.User.GetGID()
  368. cmd = wrapCmd(cmd, uid, gid)
  369. command.cmd = cmd
  370. command.realPath = path
  371. return command, nil
  372. }
  373. func (c *sshCommand) rescanHomeDir() error {
  374. quotaTracking := dataprovider.GetQuotaTracking()
  375. if (!c.connection.User.HasQuotaRestrictions() && quotaTracking == 2) || quotaTracking == 0 {
  376. return nil
  377. }
  378. var err error
  379. var numFiles int
  380. var size int64
  381. if AddQuotaScan(c.connection.User.Username) {
  382. numFiles, size, err = c.connection.fs.ScanRootDirContents()
  383. if err != nil {
  384. c.connection.Log(logger.LevelWarn, logSenderSSH, "error scanning user home dir %#v: %v", c.connection.User.HomeDir, err)
  385. } else {
  386. err := dataprovider.UpdateUserQuota(dataProvider, c.connection.User, numFiles, size, true)
  387. c.connection.Log(logger.LevelDebug, logSenderSSH, "user home dir scanned, user: %#v, dir: %#v, error: %v",
  388. c.connection.User.Username, c.connection.User.HomeDir, err)
  389. }
  390. RemoveQuotaScan(c.connection.User.Username) //nolint:errcheck
  391. }
  392. return err
  393. }
  394. // for the supported command, the path, if any, is the last argument
  395. func (c *sshCommand) getDestPath() string {
  396. if len(c.args) == 0 {
  397. return ""
  398. }
  399. destPath := strings.Trim(c.args[len(c.args)-1], "'")
  400. destPath = strings.Trim(destPath, "\"")
  401. result := utils.CleanSFTPPath(destPath)
  402. if strings.HasSuffix(destPath, "/") && !strings.HasSuffix(result, "/") {
  403. result += "/"
  404. }
  405. return result
  406. }
  407. // we try to avoid to leak the real filesystem path here
  408. func (c *sshCommand) getMappedError(err error) error {
  409. if c.connection.fs.IsNotExist(err) {
  410. return errors.New("no such file or directory")
  411. }
  412. if c.connection.fs.IsPermission(err) {
  413. return errors.New("permission denied")
  414. }
  415. return err
  416. }
  417. func (c *sshCommand) sendErrorResponse(err error) error {
  418. errorString := fmt.Sprintf("%v: %v %v\n", c.command, c.getDestPath(), c.getMappedError(err))
  419. c.connection.channel.Write([]byte(errorString)) //nolint:errcheck
  420. c.sendExitStatus(err)
  421. return err
  422. }
  423. func (c *sshCommand) sendExitStatus(err error) {
  424. status := uint32(0)
  425. if err != nil {
  426. status = uint32(1)
  427. c.connection.Log(logger.LevelWarn, logSenderSSH, "command failed: %#v args: %v user: %v err: %v",
  428. c.command, c.args, c.connection.User.Username, err)
  429. } else {
  430. logger.CommandLog(sshCommandLogSender, c.getDestPath(), "", c.connection.User.Username, "", c.connection.ID,
  431. protocolSSH, -1, -1, "", "", c.connection.command)
  432. }
  433. exitStatus := sshSubsystemExitStatus{
  434. Status: status,
  435. }
  436. c.connection.channel.SendRequest("exit-status", false, ssh.Marshal(&exitStatus)) //nolint:errcheck
  437. c.connection.channel.Close()
  438. // for scp we notify single uploads/downloads
  439. if c.command != scpCmdName {
  440. metrics.SSHCommandCompleted(err)
  441. realPath := c.getDestPath()
  442. if len(realPath) > 0 {
  443. p, e := c.connection.fs.ResolvePath(realPath)
  444. if e == nil {
  445. realPath = p
  446. }
  447. }
  448. go executeAction(newActionNotification(c.connection.User, operationSSHCmd, realPath, "", c.command, 0, err)) //nolint:errcheck
  449. }
  450. }
  451. func computeHashForFile(hasher hash.Hash, path string) (string, error) {
  452. hash := ""
  453. f, err := os.Open(path)
  454. if err != nil {
  455. return hash, err
  456. }
  457. defer f.Close()
  458. _, err = io.Copy(hasher, f)
  459. if err == nil {
  460. hash = fmt.Sprintf("%x", hasher.Sum(nil))
  461. }
  462. return hash, err
  463. }
  464. func parseCommandPayload(command string) (string, []string, error) {
  465. parts, err := shlex.Split(command)
  466. if err == nil && len(parts) == 0 {
  467. err = fmt.Errorf("invalid command: %#v", command)
  468. }
  469. if err != nil {
  470. return "", []string{}, err
  471. }
  472. if len(parts) < 2 {
  473. return parts[0], []string{}, nil
  474. }
  475. return parts[0], parts[1:], nil
  476. }