handler.go 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618
  1. package sftpd
  2. import (
  3. "fmt"
  4. "io"
  5. "io/ioutil"
  6. "net"
  7. "os"
  8. "path/filepath"
  9. "strings"
  10. "sync"
  11. "time"
  12. "github.com/drakkan/sftpgo/utils"
  13. "github.com/rs/xid"
  14. "golang.org/x/crypto/ssh"
  15. "github.com/drakkan/sftpgo/dataprovider"
  16. "github.com/drakkan/sftpgo/logger"
  17. "github.com/pkg/sftp"
  18. )
  19. // Connection details for an authenticated user
  20. type Connection struct {
  21. // Unique identifier for the connection
  22. ID string
  23. // logged in user's details
  24. User dataprovider.User
  25. // client's version string
  26. ClientVersion string
  27. // Remote address for this connection
  28. RemoteAddr net.Addr
  29. // start time for this connection
  30. StartTime time.Time
  31. // last activity for this connection
  32. lastActivity time.Time
  33. protocol string
  34. lock *sync.Mutex
  35. netConn net.Conn
  36. channel ssh.Channel
  37. }
  38. // Log outputs a log entry to the configured logger
  39. func (c Connection) Log(level logger.LogLevel, sender string, format string, v ...interface{}) {
  40. logger.Log(level, sender, c.ID, format, v...)
  41. }
  42. // Fileread creates a reader for a file on the system and returns the reader back.
  43. func (c Connection) Fileread(request *sftp.Request) (io.ReaderAt, error) {
  44. updateConnectionActivity(c.ID)
  45. if !c.User.HasPerm(dataprovider.PermDownload) {
  46. return nil, sftp.ErrSshFxPermissionDenied
  47. }
  48. p, err := c.buildPath(request.Filepath)
  49. if err != nil {
  50. return nil, sftp.ErrSshFxNoSuchFile
  51. }
  52. c.lock.Lock()
  53. defer c.lock.Unlock()
  54. if _, err := os.Stat(p); os.IsNotExist(err) {
  55. return nil, sftp.ErrSshFxNoSuchFile
  56. }
  57. file, err := os.Open(p)
  58. if err != nil {
  59. c.Log(logger.LevelError, logSender, "could not open file %#v for reading: %v", p, err)
  60. return nil, sftp.ErrSshFxFailure
  61. }
  62. c.Log(logger.LevelDebug, logSender, "fileread requested for path: %#v", p)
  63. transfer := Transfer{
  64. file: file,
  65. path: p,
  66. start: time.Now(),
  67. bytesSent: 0,
  68. bytesReceived: 0,
  69. user: c.User,
  70. connectionID: c.ID,
  71. transferType: transferDownload,
  72. lastActivity: time.Now(),
  73. isNewFile: false,
  74. protocol: c.protocol,
  75. transferError: nil,
  76. isFinished: false,
  77. }
  78. addTransfer(&transfer)
  79. return &transfer, nil
  80. }
  81. // Filewrite handles the write actions for a file on the system.
  82. func (c Connection) Filewrite(request *sftp.Request) (io.WriterAt, error) {
  83. updateConnectionActivity(c.ID)
  84. if !c.User.HasPerm(dataprovider.PermUpload) {
  85. return nil, sftp.ErrSshFxPermissionDenied
  86. }
  87. p, err := c.buildPath(request.Filepath)
  88. if err != nil {
  89. return nil, sftp.ErrSshFxNoSuchFile
  90. }
  91. filePath := p
  92. if uploadMode == uploadModeAtomic {
  93. filePath = getUploadTempFilePath(p)
  94. }
  95. c.lock.Lock()
  96. defer c.lock.Unlock()
  97. stat, statErr := os.Stat(p)
  98. // If the file doesn't exist we need to create it, as well as the directory pathway
  99. // leading up to where that file will be created.
  100. if os.IsNotExist(statErr) {
  101. return c.handleSFTPUploadToNewFile(p, filePath)
  102. }
  103. if statErr != nil {
  104. c.Log(logger.LevelError, logSender, "error performing file stat %#v: %v", p, statErr)
  105. return nil, sftp.ErrSshFxFailure
  106. }
  107. // This happen if we upload a file that has the same name of an existing directory
  108. if stat.IsDir() {
  109. c.Log(logger.LevelWarn, logSender, "attempted to open a directory for writing to: %#v", p)
  110. return nil, sftp.ErrSshFxOpUnsupported
  111. }
  112. return c.handleSFTPUploadToExistingFile(request.Pflags(), p, filePath, stat.Size())
  113. }
  114. // Filecmd hander for basic SFTP system calls related to files, but not anything to do with reading
  115. // or writing to those files.
  116. func (c Connection) Filecmd(request *sftp.Request) error {
  117. updateConnectionActivity(c.ID)
  118. p, err := c.buildPath(request.Filepath)
  119. if err != nil {
  120. return sftp.ErrSshFxNoSuchFile
  121. }
  122. target, err := c.getSFTPCmdTargetPath(request.Target)
  123. if err != nil {
  124. return sftp.ErrSshFxOpUnsupported
  125. }
  126. c.Log(logger.LevelDebug, logSender, "new cmd, method: %v, sourcePath: %#v, targetPath: %#v", request.Method,
  127. p, target)
  128. switch request.Method {
  129. case "Setstat":
  130. return nil
  131. case "Rename":
  132. err = c.handleSFTPRename(p, target)
  133. if err != nil {
  134. return err
  135. }
  136. break
  137. case "Rmdir":
  138. return c.handleSFTPRmdir(p)
  139. case "Mkdir":
  140. err = c.handleSFTPMkdir(p)
  141. if err != nil {
  142. return err
  143. }
  144. break
  145. case "Symlink":
  146. err = c.handleSFTPSymlink(p, target)
  147. if err != nil {
  148. return err
  149. }
  150. break
  151. case "Remove":
  152. return c.handleSFTPRemove(p)
  153. default:
  154. return sftp.ErrSshFxOpUnsupported
  155. }
  156. var fileLocation = p
  157. if target != "" {
  158. fileLocation = target
  159. }
  160. // we return if we remove a file or a dir so source path or target path always exists here
  161. utils.SetPathPermissions(fileLocation, c.User.GetUID(), c.User.GetGID())
  162. return sftp.ErrSshFxOk
  163. }
  164. // Filelist is the handler for SFTP filesystem list calls. This will handle calls to list the contents of
  165. // a directory as well as perform file/folder stat calls.
  166. func (c Connection) Filelist(request *sftp.Request) (sftp.ListerAt, error) {
  167. updateConnectionActivity(c.ID)
  168. p, err := c.buildPath(request.Filepath)
  169. if err != nil {
  170. return nil, sftp.ErrSshFxNoSuchFile
  171. }
  172. switch request.Method {
  173. case "List":
  174. if !c.User.HasPerm(dataprovider.PermListItems) {
  175. return nil, sftp.ErrSshFxPermissionDenied
  176. }
  177. c.Log(logger.LevelDebug, logSender, "requested list file for dir: %#v", p)
  178. files, err := ioutil.ReadDir(p)
  179. if err != nil {
  180. c.Log(logger.LevelError, logSender, "error listing directory: %#v", err)
  181. return nil, sftp.ErrSshFxFailure
  182. }
  183. return listerAt(files), nil
  184. case "Stat":
  185. if !c.User.HasPerm(dataprovider.PermListItems) {
  186. return nil, sftp.ErrSshFxPermissionDenied
  187. }
  188. c.Log(logger.LevelDebug, logSender, "requested stat for file: %#v", p)
  189. s, err := os.Stat(p)
  190. if os.IsNotExist(err) {
  191. return nil, sftp.ErrSshFxNoSuchFile
  192. } else if err != nil {
  193. c.Log(logger.LevelError, logSender, "error running STAT on file: %#v", err)
  194. return nil, sftp.ErrSshFxFailure
  195. }
  196. return listerAt([]os.FileInfo{s}), nil
  197. default:
  198. return nil, sftp.ErrSshFxOpUnsupported
  199. }
  200. }
  201. func (c Connection) getSFTPCmdTargetPath(requestTarget string) (string, error) {
  202. var target string
  203. // If a target is provided in this request validate that it is going to the correct
  204. // location for the server. If it is not, return an operation unsupported error. This
  205. // is maybe not the best error response, but its not wrong either.
  206. if requestTarget != "" {
  207. var err error
  208. target, err = c.buildPath(requestTarget)
  209. if err != nil {
  210. return target, sftp.ErrSshFxOpUnsupported
  211. }
  212. }
  213. return target, nil
  214. }
  215. func (c Connection) handleSFTPRename(sourcePath string, targetPath string) error {
  216. if !c.User.HasPerm(dataprovider.PermRename) {
  217. return sftp.ErrSshFxPermissionDenied
  218. }
  219. if err := os.Rename(sourcePath, targetPath); err != nil {
  220. c.Log(logger.LevelError, logSender, "failed to rename file, source: %#v target: %#v: %v", sourcePath, targetPath, err)
  221. return sftp.ErrSshFxFailure
  222. }
  223. logger.CommandLog(renameLogSender, sourcePath, targetPath, c.User.Username, c.ID, c.protocol)
  224. executeAction(operationRename, c.User.Username, sourcePath, targetPath)
  225. return nil
  226. }
  227. func (c Connection) handleSFTPRmdir(path string) error {
  228. if !c.User.HasPerm(dataprovider.PermDelete) {
  229. return sftp.ErrSshFxPermissionDenied
  230. }
  231. numFiles, size, fileList, err := utils.ScanDirContents(path)
  232. if err != nil {
  233. c.Log(logger.LevelError, logSender, "failed to remove directory %#v, scanning error: %v", path, err)
  234. return sftp.ErrSshFxFailure
  235. }
  236. if err := os.RemoveAll(path); err != nil {
  237. c.Log(logger.LevelError, logSender, "failed to remove directory %#v: %v", path, err)
  238. return sftp.ErrSshFxFailure
  239. }
  240. logger.CommandLog(rmdirLogSender, path, "", c.User.Username, c.ID, c.protocol)
  241. dataprovider.UpdateUserQuota(dataProvider, c.User, -numFiles, -size, false)
  242. for _, p := range fileList {
  243. executeAction(operationDelete, c.User.Username, p, "")
  244. }
  245. return sftp.ErrSshFxOk
  246. }
  247. func (c Connection) handleSFTPSymlink(sourcePath string, targetPath string) error {
  248. if !c.User.HasPerm(dataprovider.PermCreateSymlinks) {
  249. return sftp.ErrSshFxPermissionDenied
  250. }
  251. if err := os.Symlink(sourcePath, targetPath); err != nil {
  252. c.Log(logger.LevelWarn, logSender, "failed to create symlink %#v -> %#v: %v", sourcePath, targetPath, err)
  253. return sftp.ErrSshFxFailure
  254. }
  255. logger.CommandLog(symlinkLogSender, sourcePath, targetPath, c.User.Username, c.ID, c.protocol)
  256. return nil
  257. }
  258. func (c Connection) handleSFTPMkdir(path string) error {
  259. if !c.User.HasPerm(dataprovider.PermCreateDirs) {
  260. return sftp.ErrSshFxPermissionDenied
  261. }
  262. if err := c.createMissingDirs(filepath.Join(path, "testfile")); err != nil {
  263. c.Log(logger.LevelError, logSender, "error making missing dir for path %#v: %v", path, err)
  264. return sftp.ErrSshFxFailure
  265. }
  266. logger.CommandLog(mkdirLogSender, path, "", c.User.Username, c.ID, c.protocol)
  267. return nil
  268. }
  269. func (c Connection) handleSFTPRemove(path string) error {
  270. if !c.User.HasPerm(dataprovider.PermDelete) {
  271. return sftp.ErrSshFxPermissionDenied
  272. }
  273. var size int64
  274. var fi os.FileInfo
  275. var err error
  276. if fi, err = os.Lstat(path); err != nil {
  277. c.Log(logger.LevelError, logSender, "failed to remove a file %#v: stat error: %v", path, err)
  278. return sftp.ErrSshFxFailure
  279. }
  280. size = fi.Size()
  281. if err := os.Remove(path); err != nil {
  282. c.Log(logger.LevelError, logSender, "failed to remove a file/symlink %#v: %v", path, err)
  283. return sftp.ErrSshFxFailure
  284. }
  285. logger.CommandLog(removeLogSender, path, "", c.User.Username, c.ID, c.protocol)
  286. if fi.Mode()&os.ModeSymlink != os.ModeSymlink {
  287. dataprovider.UpdateUserQuota(dataProvider, c.User, -1, -size, false)
  288. }
  289. executeAction(operationDelete, c.User.Username, path, "")
  290. return sftp.ErrSshFxOk
  291. }
  292. func (c Connection) handleSFTPUploadToNewFile(requestPath, filePath string) (io.WriterAt, error) {
  293. if !c.hasSpace(true) {
  294. c.Log(logger.LevelInfo, logSender, "denying file write due to space limit")
  295. return nil, sftp.ErrSshFxFailure
  296. }
  297. if _, err := os.Stat(filepath.Dir(requestPath)); os.IsNotExist(err) {
  298. if !c.User.HasPerm(dataprovider.PermCreateDirs) {
  299. return nil, sftp.ErrSshFxPermissionDenied
  300. }
  301. }
  302. err := c.createMissingDirs(requestPath)
  303. if err != nil {
  304. c.Log(logger.LevelError, logSender, "error making missing dir for path %#v: %v", requestPath, err)
  305. return nil, sftp.ErrSshFxFailure
  306. }
  307. file, err := os.Create(filePath)
  308. if err != nil {
  309. c.Log(logger.LevelError, logSender, "error creating file %#v: %v", requestPath, err)
  310. return nil, sftp.ErrSshFxFailure
  311. }
  312. utils.SetPathPermissions(filePath, c.User.GetUID(), c.User.GetGID())
  313. transfer := Transfer{
  314. file: file,
  315. path: requestPath,
  316. start: time.Now(),
  317. bytesSent: 0,
  318. bytesReceived: 0,
  319. user: c.User,
  320. connectionID: c.ID,
  321. transferType: transferUpload,
  322. lastActivity: time.Now(),
  323. isNewFile: true,
  324. protocol: c.protocol,
  325. transferError: nil,
  326. isFinished: false,
  327. }
  328. addTransfer(&transfer)
  329. return &transfer, nil
  330. }
  331. func (c Connection) handleSFTPUploadToExistingFile(pflags sftp.FileOpenFlags, requestPath, filePath string,
  332. fileSize int64) (io.WriterAt, error) {
  333. var err error
  334. if !c.hasSpace(false) {
  335. c.Log(logger.LevelInfo, logSender, "denying file write due to space limit")
  336. return nil, sftp.ErrSshFxFailure
  337. }
  338. osFlags := getOSOpenFlags(pflags)
  339. if osFlags&os.O_TRUNC == 0 {
  340. // see https://github.com/pkg/sftp/issues/295
  341. c.Log(logger.LevelInfo, logSender, "upload resume is not supported, returning error for file: %#v", requestPath)
  342. return nil, sftp.ErrSshFxOpUnsupported
  343. }
  344. if uploadMode == uploadModeAtomic {
  345. err = os.Rename(requestPath, filePath)
  346. if err != nil {
  347. c.Log(logger.LevelError, logSender, "error renaming existing file for atomic upload, source: %#v, dest: %#v, err: %v",
  348. requestPath, filePath, err)
  349. return nil, sftp.ErrSshFxFailure
  350. }
  351. }
  352. // we use 0666 so the umask is applied
  353. file, err := os.OpenFile(filePath, osFlags, 0666)
  354. if err != nil {
  355. c.Log(logger.LevelError, logSender, "error opening existing file, flags: %v, source: %#v, err: %v", pflags, filePath, err)
  356. return nil, sftp.ErrSshFxFailure
  357. }
  358. // FIXME: this need to be changed when we add upload resume support
  359. // the file is truncated so we need to decrease quota size but not quota files
  360. dataprovider.UpdateUserQuota(dataProvider, c.User, 0, -fileSize, false)
  361. utils.SetPathPermissions(filePath, c.User.GetUID(), c.User.GetGID())
  362. transfer := Transfer{
  363. file: file,
  364. path: requestPath,
  365. start: time.Now(),
  366. bytesSent: 0,
  367. bytesReceived: 0,
  368. user: c.User,
  369. connectionID: c.ID,
  370. transferType: transferUpload,
  371. lastActivity: time.Now(),
  372. isNewFile: false,
  373. protocol: c.protocol,
  374. transferError: nil,
  375. isFinished: false,
  376. }
  377. addTransfer(&transfer)
  378. return &transfer, nil
  379. }
  380. func (c Connection) hasSpace(checkFiles bool) bool {
  381. if (checkFiles && c.User.QuotaFiles > 0) || c.User.QuotaSize > 0 {
  382. numFile, size, err := dataprovider.GetUsedQuota(dataProvider, c.User.Username)
  383. if err != nil {
  384. if _, ok := err.(*dataprovider.MethodDisabledError); ok {
  385. c.Log(logger.LevelWarn, logSender, "quota enforcement not possible for user %v: %v", c.User.Username, err)
  386. return true
  387. }
  388. c.Log(logger.LevelWarn, logSender, "error getting used quota for %v: %v", c.User.Username, err)
  389. return false
  390. }
  391. if (checkFiles && c.User.QuotaFiles > 0 && numFile >= c.User.QuotaFiles) ||
  392. (c.User.QuotaSize > 0 && size >= c.User.QuotaSize) {
  393. c.Log(logger.LevelDebug, logSender, "quota exceed for user %v, num files: %v/%v, size: %v/%v check files: %v",
  394. c.User.Username, numFile, c.User.QuotaFiles, size, c.User.QuotaSize, checkFiles)
  395. return false
  396. }
  397. }
  398. return true
  399. }
  400. // Normalizes a directory we get from the SFTP request to ensure the user is not able to escape
  401. // from their data directory. After normalization if the directory is still within their home
  402. // path it is returned. If they managed to "escape" an error will be returned.
  403. func (c Connection) buildPath(rawPath string) (string, error) {
  404. r := filepath.Clean(filepath.Join(c.User.HomeDir, rawPath))
  405. p, err := filepath.EvalSymlinks(r)
  406. if err != nil && !os.IsNotExist(err) {
  407. return "", err
  408. } else if os.IsNotExist(err) {
  409. // The requested directory doesn't exist, so at this point we need to iterate up the
  410. // path chain until we hit a directory that _does_ exist and can be validated.
  411. _, err = c.findFirstExistingDir(r)
  412. if err != nil {
  413. c.Log(logger.LevelWarn, logSender, "error resolving not existent path: %#v", err)
  414. }
  415. return r, err
  416. }
  417. err = c.isSubDir(p)
  418. if err != nil {
  419. c.Log(logger.LevelWarn, logSender, "Invalid path resolution, dir: %#v outside user home: %#v err: %v", p, c.User.HomeDir, err)
  420. }
  421. return r, err
  422. }
  423. // iterate up the path chain until we hit a directory that does exist and can be validated.
  424. // all nonexistent directories will be returned
  425. func (c Connection) findNonexistentDirs(path string) ([]string, error) {
  426. results := []string{}
  427. cleanPath := filepath.Clean(path)
  428. parent := filepath.Dir(cleanPath)
  429. _, err := os.Stat(parent)
  430. for os.IsNotExist(err) {
  431. results = append(results, parent)
  432. parent = filepath.Dir(parent)
  433. _, err = os.Stat(parent)
  434. }
  435. if err != nil {
  436. return results, err
  437. }
  438. p, err := filepath.EvalSymlinks(parent)
  439. if err != nil {
  440. return results, err
  441. }
  442. err = c.isSubDir(p)
  443. if err != nil {
  444. c.Log(logger.LevelWarn, logSender, "Error finding non existing dir: %v", err)
  445. }
  446. return results, err
  447. }
  448. // iterate up the path chain until we hit a directory that does exist and can be validated.
  449. func (c Connection) findFirstExistingDir(path string) (string, error) {
  450. results, err := c.findNonexistentDirs(path)
  451. if err != nil {
  452. c.Log(logger.LevelWarn, logSender, "unable to find non existent dirs: %v", err)
  453. return "", err
  454. }
  455. var parent string
  456. if len(results) > 0 {
  457. lastMissingDir := results[len(results)-1]
  458. parent = filepath.Dir(lastMissingDir)
  459. } else {
  460. parent = c.User.GetHomeDir()
  461. }
  462. p, err := filepath.EvalSymlinks(parent)
  463. if err != nil {
  464. return "", err
  465. }
  466. fileInfo, err := os.Stat(p)
  467. if err != nil {
  468. return "", err
  469. }
  470. if !fileInfo.IsDir() {
  471. return "", fmt.Errorf("resolved path is not a dir: %#v", p)
  472. }
  473. err = c.isSubDir(p)
  474. return p, err
  475. }
  476. // checks if sub is a subpath of the user home dir.
  477. // EvalSymlink must be used on sub before calling this method
  478. func (c Connection) isSubDir(sub string) error {
  479. // home dir must exist and it is already a validated absolute path
  480. parent, err := filepath.EvalSymlinks(c.User.HomeDir)
  481. if err != nil {
  482. c.Log(logger.LevelWarn, logSender, "invalid home dir %#v: %v", c.User.HomeDir, err)
  483. return err
  484. }
  485. if !strings.HasPrefix(sub, parent) {
  486. c.Log(logger.LevelWarn, logSender, "dir %#v is not inside: %#v ", sub, parent)
  487. return fmt.Errorf("dir %#v is not inside: %#v", sub, parent)
  488. }
  489. return nil
  490. }
  491. func (c Connection) createMissingDirs(filePath string) error {
  492. dirsToCreate, err := c.findNonexistentDirs(filePath)
  493. if err != nil {
  494. return err
  495. }
  496. last := len(dirsToCreate) - 1
  497. for i := range dirsToCreate {
  498. d := dirsToCreate[last-i]
  499. if err := os.Mkdir(d, 0777); err != nil {
  500. c.Log(logger.LevelError, logSender, "error creating missing dir: %#v", d)
  501. return err
  502. }
  503. utils.SetPathPermissions(d, c.User.GetUID(), c.User.GetGID())
  504. }
  505. return nil
  506. }
  507. func (c Connection) close() error {
  508. if c.channel != nil {
  509. err := c.channel.Close()
  510. c.Log(logger.LevelInfo, logSender, "channel close, err: %v", err)
  511. }
  512. return c.netConn.Close()
  513. }
  514. func getOSOpenFlags(requestFlags sftp.FileOpenFlags) (flags int) {
  515. var osFlags int
  516. if requestFlags.Read && requestFlags.Write {
  517. osFlags |= os.O_RDWR
  518. } else if requestFlags.Write {
  519. osFlags |= os.O_WRONLY
  520. }
  521. if requestFlags.Append {
  522. osFlags |= os.O_APPEND
  523. }
  524. if requestFlags.Creat {
  525. osFlags |= os.O_CREATE
  526. }
  527. if requestFlags.Trunc {
  528. osFlags |= os.O_TRUNC
  529. }
  530. if requestFlags.Excl {
  531. osFlags |= os.O_EXCL
  532. }
  533. return osFlags
  534. }
  535. func getUploadTempFilePath(path string) string {
  536. dir := filepath.Dir(path)
  537. guid := xid.New().String()
  538. return filepath.Join(dir, ".sftpgo-upload."+guid+"."+filepath.Base(path))
  539. }