handler.go 17 KB

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