middleware.go 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  1. // Copyright (C) 2019 Nicola Murino
  2. //
  3. // This program is free software: you can redistribute it and/or modify
  4. // it under the terms of the GNU Affero General Public License as published
  5. // by the Free Software Foundation, version 3.
  6. //
  7. // This program is distributed in the hope that it will be useful,
  8. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. // GNU Affero General Public License for more details.
  11. //
  12. // You should have received a copy of the GNU Affero General Public License
  13. // along with this program. If not, see <https://www.gnu.org/licenses/>.
  14. package sftpd
  15. import (
  16. "io"
  17. "os"
  18. "path"
  19. "strings"
  20. "time"
  21. "github.com/pkg/sftp"
  22. "github.com/drakkan/sftpgo/v2/internal/vfs"
  23. )
  24. // Middleware defines the interface for SFTP middlewares
  25. type Middleware interface {
  26. sftp.FileReader
  27. sftp.FileWriter
  28. sftp.OpenFileWriter
  29. sftp.FileCmder
  30. sftp.StatVFSFileCmder
  31. sftp.FileLister
  32. sftp.LstatFileLister
  33. }
  34. type prefixMatch uint8
  35. const (
  36. pathContainsPrefix prefixMatch = iota
  37. pathIsPrefixParent
  38. pathDiverged
  39. methodList = "List"
  40. methodStat = "Stat"
  41. )
  42. type prefixMiddleware struct {
  43. prefix string
  44. next Middleware
  45. }
  46. func newPrefixMiddleware(prefix string, next Middleware) Middleware {
  47. return &prefixMiddleware{
  48. prefix: prefix,
  49. next: next,
  50. }
  51. }
  52. func (p *prefixMiddleware) Lstat(request *sftp.Request) (sftp.ListerAt, error) {
  53. switch getPrefixHierarchy(p.prefix, request.Filepath) {
  54. case pathContainsPrefix:
  55. request.Filepath, _ = p.removeFolderPrefix(request.Filepath)
  56. return p.next.Lstat(request)
  57. case pathIsPrefixParent:
  58. return listerAt([]os.FileInfo{
  59. vfs.NewFileInfo(request.Filepath, true, 0, time.Unix(0, 0), false),
  60. }), nil
  61. default:
  62. return nil, sftp.ErrSSHFxPermissionDenied
  63. }
  64. }
  65. func (p *prefixMiddleware) OpenFile(request *sftp.Request) (sftp.WriterAtReaderAt, error) {
  66. switch getPrefixHierarchy(p.prefix, request.Filepath) {
  67. case pathContainsPrefix:
  68. request.Filepath, _ = p.removeFolderPrefix(request.Filepath)
  69. return p.next.OpenFile(request)
  70. default:
  71. return nil, sftp.ErrSSHFxPermissionDenied
  72. }
  73. }
  74. func (p *prefixMiddleware) Filelist(request *sftp.Request) (sftp.ListerAt, error) {
  75. switch getPrefixHierarchy(p.prefix, request.Filepath) {
  76. case pathContainsPrefix:
  77. request.Filepath, _ = p.removeFolderPrefix(request.Filepath)
  78. return p.next.Filelist(request)
  79. case pathIsPrefixParent:
  80. switch request.Method {
  81. case methodList:
  82. modTime := time.Unix(0, 0)
  83. fileName := p.nextListFolder(request.Filepath)
  84. files := make([]os.FileInfo, 0, 3)
  85. files = append(files, vfs.NewFileInfo(".", true, 0, modTime, false))
  86. if request.Filepath != "/" {
  87. files = append(files, vfs.NewFileInfo("..", true, 0, modTime, false))
  88. }
  89. files = append(files, vfs.NewFileInfo(fileName, true, 0, modTime, false))
  90. return listerAt(files), nil
  91. case methodStat:
  92. return listerAt([]os.FileInfo{
  93. vfs.NewFileInfo(request.Filepath, true, 0, time.Unix(0, 0), false),
  94. }), nil
  95. default:
  96. return nil, sftp.ErrSSHFxOpUnsupported
  97. }
  98. default:
  99. return nil, sftp.ErrSSHFxPermissionDenied
  100. }
  101. }
  102. func (p *prefixMiddleware) Filewrite(request *sftp.Request) (io.WriterAt, error) {
  103. switch getPrefixHierarchy(p.prefix, request.Filepath) {
  104. case pathContainsPrefix:
  105. // forward to next handler
  106. request.Filepath, _ = p.removeFolderPrefix(request.Filepath)
  107. return p.next.Filewrite(request)
  108. default:
  109. return nil, sftp.ErrSSHFxPermissionDenied
  110. }
  111. }
  112. func (p *prefixMiddleware) Fileread(request *sftp.Request) (io.ReaderAt, error) {
  113. switch getPrefixHierarchy(p.prefix, request.Filepath) {
  114. case pathContainsPrefix:
  115. request.Filepath, _ = p.removeFolderPrefix(request.Filepath)
  116. return p.next.Fileread(request)
  117. default:
  118. return nil, sftp.ErrSSHFxPermissionDenied
  119. }
  120. }
  121. func (p *prefixMiddleware) Filecmd(request *sftp.Request) error {
  122. switch request.Method {
  123. case "Rename", "Symlink":
  124. if getPrefixHierarchy(p.prefix, request.Filepath) == pathContainsPrefix &&
  125. getPrefixHierarchy(p.prefix, request.Target) == pathContainsPrefix {
  126. request.Filepath, _ = p.removeFolderPrefix(request.Filepath)
  127. request.Target, _ = p.removeFolderPrefix(request.Target)
  128. return p.next.Filecmd(request)
  129. }
  130. return sftp.ErrSSHFxPermissionDenied
  131. // commands have a source and destination (file path and target path)
  132. case "Setstat", "Rmdir", "Mkdir", "Remove":
  133. // commands just the file path
  134. if getPrefixHierarchy(p.prefix, request.Filepath) == pathContainsPrefix {
  135. request.Filepath, _ = p.removeFolderPrefix(request.Filepath)
  136. return p.next.Filecmd(request)
  137. }
  138. return sftp.ErrSSHFxPermissionDenied
  139. default:
  140. return sftp.ErrSSHFxOpUnsupported
  141. }
  142. }
  143. func (p *prefixMiddleware) StatVFS(request *sftp.Request) (*sftp.StatVFS, error) {
  144. switch getPrefixHierarchy(p.prefix, request.Filepath) {
  145. case pathContainsPrefix:
  146. // forward to next handler
  147. request.Filepath, _ = p.removeFolderPrefix(request.Filepath)
  148. return p.next.StatVFS(request)
  149. default:
  150. return nil, sftp.ErrSSHFxPermissionDenied
  151. }
  152. }
  153. func (p *prefixMiddleware) nextListFolder(requestPath string) string {
  154. cleanPath := path.Clean(`/` + requestPath)
  155. cleanPrefix := path.Clean(`/` + p.prefix)
  156. fileName := cleanPrefix[len(cleanPath):]
  157. fileName = strings.TrimLeft(fileName, `/`)
  158. slashIndex := strings.Index(fileName, `/`)
  159. if slashIndex > 0 {
  160. return fileName[0:slashIndex]
  161. }
  162. return fileName
  163. }
  164. func (p *prefixMiddleware) containsPrefix(virtualPath string) bool {
  165. if !path.IsAbs(virtualPath) {
  166. virtualPath = path.Clean(`/` + virtualPath)
  167. }
  168. if p.prefix == `/` || p.prefix == `` {
  169. return true
  170. } else if p.prefix == virtualPath {
  171. return true
  172. }
  173. return strings.HasPrefix(virtualPath, p.prefix+`/`)
  174. }
  175. func (p *prefixMiddleware) removeFolderPrefix(virtualPath string) (string, bool) {
  176. if p.prefix == `/` || p.prefix == `` {
  177. return virtualPath, true
  178. }
  179. virtualPath = path.Clean(`/` + virtualPath)
  180. if p.containsPrefix(virtualPath) {
  181. effectivePath := virtualPath[len(p.prefix):]
  182. if effectivePath == `` {
  183. effectivePath = `/`
  184. }
  185. return effectivePath, true
  186. }
  187. return virtualPath, false
  188. }
  189. func getPrefixHierarchy(prefix, virtualPath string) prefixMatch {
  190. prefixSplit := strings.Split(path.Clean(`/`+prefix), `/`)
  191. pathSplit := strings.Split(path.Clean(`/`+virtualPath), `/`)
  192. for {
  193. // stop if either slice is empty of the current head elements do not match
  194. if len(prefixSplit) == 0 || len(pathSplit) == 0 ||
  195. prefixSplit[0] != pathSplit[0] {
  196. break
  197. }
  198. prefixSplit = prefixSplit[1:]
  199. pathSplit = pathSplit[1:]
  200. }
  201. // The entire Prefix is included in Test Path
  202. // Example: Prefix (/files) with Test Path (/files/test.csv)
  203. if len(prefixSplit) == 0 ||
  204. (len(prefixSplit) == 1 && prefixSplit[0] == ``) {
  205. return pathContainsPrefix
  206. }
  207. // Test Path is part of the Prefix Hierarchy
  208. // Example: Prefix (/files) with Test Path (/)
  209. if len(pathSplit) == 0 ||
  210. (len(pathSplit) == 1 && pathSplit[0] == ``) {
  211. return pathIsPrefixParent
  212. }
  213. // Test Path is not with the Prefix Hierarchy
  214. // Example: Prefix (/files) with Test Path (/files2)
  215. return pathDiverged
  216. }