ssh_cmd.go 15 KB

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