httpfs_test.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365
  1. // Copyright (C) 2019-2022 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_test
  15. import (
  16. "fmt"
  17. "io/fs"
  18. "math"
  19. "net/http"
  20. "net/url"
  21. "os"
  22. "path"
  23. "path/filepath"
  24. "runtime"
  25. "testing"
  26. "time"
  27. "github.com/sftpgo/sdk"
  28. "github.com/stretchr/testify/assert"
  29. "github.com/stretchr/testify/require"
  30. "github.com/drakkan/sftpgo/v2/dataprovider"
  31. "github.com/drakkan/sftpgo/v2/httpdtest"
  32. "github.com/drakkan/sftpgo/v2/logger"
  33. "github.com/drakkan/sftpgo/v2/vfs"
  34. )
  35. const (
  36. httpFsPort = 12345
  37. defaultHTTPFsUsername = "httpfs_user"
  38. )
  39. var (
  40. httpFsSocketPath = filepath.Join(os.TempDir(), "httpfs.sock")
  41. )
  42. func TestBasicHTTPFsHandling(t *testing.T) {
  43. usePubKey := true
  44. u := getTestUserWithHTTPFs(usePubKey)
  45. u.QuotaSize = 6553600
  46. user, _, err := httpdtest.AddUser(u, http.StatusCreated)
  47. assert.NoError(t, err)
  48. conn, client, err := getSftpClient(user, usePubKey)
  49. if assert.NoError(t, err) {
  50. defer conn.Close()
  51. defer client.Close()
  52. testFilePath := filepath.Join(homeBasePath, testFileName)
  53. testFileSize := int64(65535)
  54. expectedQuotaSize := user.UsedQuotaSize + testFileSize*2
  55. expectedQuotaFiles := user.UsedQuotaFiles + 2
  56. err = createTestFile(testFilePath, testFileSize)
  57. assert.NoError(t, err)
  58. err = sftpUploadFile(testFilePath, path.Join("/missing_dir", testFileName), testFileSize, client)
  59. assert.Error(t, err)
  60. err = sftpUploadFile(testFilePath, testFileName, testFileSize, client)
  61. assert.NoError(t, err)
  62. info, err := client.Stat(testFileName)
  63. if assert.NoError(t, err) {
  64. assert.Equal(t, testFileSize, info.Size())
  65. }
  66. contents, err := client.ReadDir("/")
  67. assert.NoError(t, err)
  68. if assert.Len(t, contents, 1) {
  69. assert.Equal(t, testFileName, contents[0].Name())
  70. }
  71. dirName := "test dirname"
  72. err = client.Mkdir(dirName)
  73. assert.NoError(t, err)
  74. contents, err = client.ReadDir(".")
  75. assert.NoError(t, err)
  76. assert.Len(t, contents, 2)
  77. contents, err = client.ReadDir(dirName)
  78. assert.NoError(t, err)
  79. assert.Len(t, contents, 0)
  80. err = sftpUploadFile(testFilePath, path.Join(dirName, testFileName), testFileSize, client)
  81. assert.NoError(t, err)
  82. contents, err = client.ReadDir(dirName)
  83. assert.NoError(t, err)
  84. assert.Len(t, contents, 1)
  85. dirRenamed := dirName + "_renamed"
  86. err = client.Rename(dirName, dirRenamed)
  87. assert.NoError(t, err)
  88. info, err = client.Stat(dirRenamed)
  89. if assert.NoError(t, err) {
  90. assert.True(t, info.IsDir())
  91. }
  92. // mode 0666 and 0444 works on Windows too
  93. newPerm := os.FileMode(0444)
  94. err = client.Chmod(testFileName, newPerm)
  95. assert.NoError(t, err)
  96. info, err = client.Stat(testFileName)
  97. assert.NoError(t, err)
  98. assert.Equal(t, newPerm, info.Mode().Perm())
  99. newPerm = os.FileMode(0666)
  100. err = client.Chmod(testFileName, newPerm)
  101. assert.NoError(t, err)
  102. info, err = client.Stat(testFileName)
  103. assert.NoError(t, err)
  104. assert.Equal(t, newPerm, info.Mode().Perm())
  105. // chtimes
  106. acmodTime := time.Now().Add(-36 * time.Hour)
  107. err = client.Chtimes(testFileName, acmodTime, acmodTime)
  108. assert.NoError(t, err)
  109. info, err = client.Stat(testFileName)
  110. if assert.NoError(t, err) {
  111. diff := math.Abs(info.ModTime().Sub(acmodTime).Seconds())
  112. assert.LessOrEqual(t, diff, float64(1))
  113. }
  114. _, err = client.StatVFS("/")
  115. assert.NoError(t, err)
  116. localDownloadPath := filepath.Join(homeBasePath, testDLFileName)
  117. err = sftpDownloadFile(testFileName, localDownloadPath, testFileSize, client)
  118. assert.NoError(t, err)
  119. user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
  120. assert.NoError(t, err)
  121. assert.Equal(t, expectedQuotaFiles, user.UsedQuotaFiles)
  122. assert.Equal(t, expectedQuotaSize, user.UsedQuotaSize)
  123. // execute a quota scan
  124. _, err = httpdtest.StartQuotaScan(user, http.StatusAccepted)
  125. assert.NoError(t, err)
  126. assert.Eventually(t, func() bool {
  127. scans, _, err := httpdtest.GetQuotaScans(http.StatusOK)
  128. if err == nil {
  129. return len(scans) == 0
  130. }
  131. return false
  132. }, 1*time.Second, 50*time.Millisecond)
  133. err = client.Remove(testFileName)
  134. assert.NoError(t, err)
  135. _, err = client.Lstat(testFileName)
  136. assert.Error(t, err)
  137. user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
  138. assert.NoError(t, err)
  139. assert.Equal(t, expectedQuotaFiles-1, user.UsedQuotaFiles)
  140. assert.Equal(t, expectedQuotaSize-testFileSize, user.UsedQuotaSize)
  141. // truncate
  142. err = client.Truncate(path.Join(dirRenamed, testFileName), 100)
  143. assert.NoError(t, err)
  144. info, err = client.Stat(path.Join(dirRenamed, testFileName))
  145. if assert.NoError(t, err) {
  146. assert.Equal(t, int64(100), info.Size())
  147. }
  148. user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
  149. assert.NoError(t, err)
  150. assert.Equal(t, expectedQuotaFiles-1, user.UsedQuotaFiles)
  151. assert.Equal(t, int64(100), user.UsedQuotaSize)
  152. // update quota
  153. _, err = httpdtest.StartQuotaScan(user, http.StatusAccepted)
  154. assert.NoError(t, err)
  155. assert.Eventually(t, func() bool {
  156. scans, _, err := httpdtest.GetQuotaScans(http.StatusOK)
  157. if err == nil {
  158. return len(scans) == 0
  159. }
  160. return false
  161. }, 1*time.Second, 50*time.Millisecond)
  162. user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
  163. assert.NoError(t, err)
  164. assert.Equal(t, expectedQuotaFiles-1, user.UsedQuotaFiles)
  165. assert.Equal(t, int64(100), user.UsedQuotaSize)
  166. err = os.Remove(testFilePath)
  167. assert.NoError(t, err)
  168. err = os.Remove(localDownloadPath)
  169. assert.NoError(t, err)
  170. }
  171. _, err = httpdtest.RemoveUser(user, http.StatusOK)
  172. assert.NoError(t, err)
  173. err = os.RemoveAll(user.GetHomeDir())
  174. assert.NoError(t, err)
  175. }
  176. func TestHTTPFsVirtualFolder(t *testing.T) {
  177. usePubKey := false
  178. u := getTestUser(usePubKey)
  179. folderName := "httpfsfolder"
  180. vdirPath := "/vdir/http fs"
  181. u.VirtualFolders = append(u.VirtualFolders, vfs.VirtualFolder{
  182. BaseVirtualFolder: vfs.BaseVirtualFolder{
  183. Name: folderName,
  184. FsConfig: vfs.Filesystem{
  185. Provider: sdk.HTTPFilesystemProvider,
  186. HTTPConfig: vfs.HTTPFsConfig{
  187. BaseHTTPFsConfig: sdk.BaseHTTPFsConfig{
  188. Endpoint: fmt.Sprintf("http://127.0.0.1:%d/api/v1", httpFsPort),
  189. Username: defaultHTTPFsUsername,
  190. },
  191. },
  192. },
  193. },
  194. VirtualPath: vdirPath,
  195. })
  196. user, _, err := httpdtest.AddUser(u, http.StatusCreated)
  197. assert.NoError(t, err)
  198. conn, client, err := getSftpClient(user, usePubKey)
  199. if assert.NoError(t, err) {
  200. defer conn.Close()
  201. defer client.Close()
  202. testFilePath := filepath.Join(homeBasePath, testFileName)
  203. testFileSize := int64(65535)
  204. err = createTestFile(testFilePath, testFileSize)
  205. assert.NoError(t, err)
  206. err = sftpUploadFile(testFilePath, path.Join(vdirPath, testFileName), testFileSize, client)
  207. assert.NoError(t, err)
  208. _, err = client.Stat(path.Join(vdirPath, testFileName))
  209. assert.NoError(t, err)
  210. localDownloadPath := filepath.Join(homeBasePath, testDLFileName)
  211. err = sftpDownloadFile(path.Join(vdirPath, testFileName), localDownloadPath, testFileSize, client)
  212. assert.NoError(t, err)
  213. err = os.Remove(testFilePath)
  214. assert.NoError(t, err)
  215. err = os.Remove(localDownloadPath)
  216. assert.NoError(t, err)
  217. }
  218. _, err = httpdtest.RemoveUser(user, http.StatusOK)
  219. assert.NoError(t, err)
  220. err = os.RemoveAll(user.GetHomeDir())
  221. assert.NoError(t, err)
  222. _, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderName}, http.StatusOK)
  223. assert.NoError(t, err)
  224. }
  225. func TestHTTPFsWalk(t *testing.T) {
  226. user := getTestUserWithHTTPFs(false)
  227. httpFs, err := user.GetFilesystem("")
  228. require.NoError(t, err)
  229. basePath := filepath.Join(os.TempDir(), "httpfs", user.FsConfig.HTTPConfig.Username)
  230. err = os.RemoveAll(basePath)
  231. assert.NoError(t, err)
  232. var walkedPaths []string
  233. err = httpFs.Walk("/", func(walkedPath string, _ fs.FileInfo, err error) error {
  234. if err != nil {
  235. return err
  236. }
  237. walkedPaths = append(walkedPaths, httpFs.GetRelativePath(walkedPath))
  238. return nil
  239. })
  240. require.NoError(t, err)
  241. require.Len(t, walkedPaths, 1)
  242. require.Contains(t, walkedPaths, "/")
  243. // now add some files/folders
  244. for i := 0; i < 10; i++ {
  245. err = os.WriteFile(filepath.Join(basePath, fmt.Sprintf("file%d", i)), nil, os.ModePerm)
  246. assert.NoError(t, err)
  247. err = os.Mkdir(filepath.Join(basePath, fmt.Sprintf("dir%d", i)), os.ModePerm)
  248. assert.NoError(t, err)
  249. for j := 0; j < 5; j++ {
  250. err = os.WriteFile(filepath.Join(basePath, fmt.Sprintf("dir%d", i), fmt.Sprintf("subfile%d", j)), nil, os.ModePerm)
  251. assert.NoError(t, err)
  252. }
  253. }
  254. walkedPaths = nil
  255. err = httpFs.Walk("/", func(walkedPath string, _ fs.FileInfo, err error) error {
  256. if err != nil {
  257. return err
  258. }
  259. walkedPaths = append(walkedPaths, httpFs.GetRelativePath(walkedPath))
  260. return nil
  261. })
  262. require.NoError(t, err)
  263. require.Len(t, walkedPaths, 71)
  264. require.Contains(t, walkedPaths, "/")
  265. for i := 0; i < 10; i++ {
  266. require.Contains(t, walkedPaths, path.Join("/", fmt.Sprintf("file%d", i)))
  267. require.Contains(t, walkedPaths, path.Join("/", fmt.Sprintf("dir%d", i)))
  268. for j := 0; j < 5; j++ {
  269. require.Contains(t, walkedPaths, path.Join("/", fmt.Sprintf("dir%d", i), fmt.Sprintf("subfile%d", j)))
  270. }
  271. }
  272. err = os.RemoveAll(basePath)
  273. assert.NoError(t, err)
  274. }
  275. func TestHTTPFsOverUNIXSocket(t *testing.T) {
  276. if runtime.GOOS == osWindows {
  277. t.Skip("UNIX domain sockets are not supported on Windows")
  278. }
  279. assert.Eventually(t, func() bool {
  280. _, err := os.Stat(httpFsSocketPath)
  281. return err == nil
  282. }, 1*time.Second, 50*time.Millisecond)
  283. usePubKey := true
  284. u := getTestUserWithHTTPFs(usePubKey)
  285. u.FsConfig.HTTPConfig.Endpoint = fmt.Sprintf("http://unix?socket_path=%s&api_prefix=%s",
  286. url.QueryEscape(httpFsSocketPath), url.QueryEscape("/api/v1"))
  287. user, _, err := httpdtest.AddUser(u, http.StatusCreated)
  288. assert.NoError(t, err)
  289. conn, client, err := getSftpClient(user, usePubKey)
  290. if assert.NoError(t, err) {
  291. defer conn.Close()
  292. defer client.Close()
  293. err = checkBasicSFTP(client)
  294. assert.NoError(t, err)
  295. testFilePath := filepath.Join(homeBasePath, testFileName)
  296. testFileSize := int64(65535)
  297. err = createTestFile(testFilePath, testFileSize)
  298. assert.NoError(t, err)
  299. err = sftpUploadFile(testFilePath, testFileName, testFileSize, client)
  300. assert.NoError(t, err)
  301. err = client.Remove(testFileName)
  302. assert.NoError(t, err)
  303. err = client.Mkdir(testFileName)
  304. assert.NoError(t, err)
  305. err = client.RemoveDirectory(testFileName)
  306. assert.NoError(t, err)
  307. err = os.Remove(testFilePath)
  308. assert.NoError(t, err)
  309. }
  310. _, err = httpdtest.RemoveUser(user, http.StatusOK)
  311. assert.NoError(t, err)
  312. err = os.RemoveAll(user.GetHomeDir())
  313. assert.NoError(t, err)
  314. }
  315. func getTestUserWithHTTPFs(usePubKey bool) dataprovider.User {
  316. u := getTestUser(usePubKey)
  317. u.FsConfig.Provider = sdk.HTTPFilesystemProvider
  318. u.FsConfig.HTTPConfig = vfs.HTTPFsConfig{
  319. BaseHTTPFsConfig: sdk.BaseHTTPFsConfig{
  320. Endpoint: fmt.Sprintf("http://127.0.0.1:%d/api/v1", httpFsPort),
  321. Username: defaultHTTPFsUsername,
  322. },
  323. }
  324. return u
  325. }
  326. func startHTTPFs() {
  327. if runtime.GOOS != osWindows {
  328. go func() {
  329. if err := httpdtest.StartTestHTTPFsOverUnixSocket(httpFsSocketPath); err != nil {
  330. logger.ErrorToConsole("could not start HTTPfs test server over UNIX socket: %v", err)
  331. os.Exit(1)
  332. }
  333. }()
  334. }
  335. go func() {
  336. if err := httpdtest.StartTestHTTPFs(httpFsPort); err != nil {
  337. logger.ErrorToConsole("could not start HTTPfs test server: %v", err)
  338. os.Exit(1)
  339. }
  340. }()
  341. waitTCPListening(fmt.Sprintf(":%d", httpFsPort))
  342. }