httpfs_test.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367
  1. // Copyright (C) 2019-2023 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/internal/dataprovider"
  31. "github.com/drakkan/sftpgo/v2/internal/httpdtest"
  32. "github.com/drakkan/sftpgo/v2/internal/logger"
  33. "github.com/drakkan/sftpgo/v2/internal/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. EqualityCheckMode: 1,
  191. },
  192. },
  193. },
  194. },
  195. VirtualPath: vdirPath,
  196. })
  197. user, _, err := httpdtest.AddUser(u, http.StatusCreated)
  198. assert.NoError(t, err)
  199. conn, client, err := getSftpClient(user, usePubKey)
  200. if assert.NoError(t, err) {
  201. defer conn.Close()
  202. defer client.Close()
  203. testFilePath := filepath.Join(homeBasePath, testFileName)
  204. testFileSize := int64(65535)
  205. err = createTestFile(testFilePath, testFileSize)
  206. assert.NoError(t, err)
  207. err = sftpUploadFile(testFilePath, path.Join(vdirPath, testFileName), testFileSize, client)
  208. assert.NoError(t, err)
  209. _, err = client.Stat(path.Join(vdirPath, testFileName))
  210. assert.NoError(t, err)
  211. localDownloadPath := filepath.Join(homeBasePath, testDLFileName)
  212. err = sftpDownloadFile(path.Join(vdirPath, testFileName), localDownloadPath, testFileSize, client)
  213. assert.NoError(t, err)
  214. err = os.Remove(testFilePath)
  215. assert.NoError(t, err)
  216. err = os.Remove(localDownloadPath)
  217. assert.NoError(t, err)
  218. }
  219. _, err = httpdtest.RemoveUser(user, http.StatusOK)
  220. assert.NoError(t, err)
  221. err = os.RemoveAll(user.GetHomeDir())
  222. assert.NoError(t, err)
  223. _, err = httpdtest.RemoveFolder(vfs.BaseVirtualFolder{Name: folderName}, http.StatusOK)
  224. assert.NoError(t, err)
  225. }
  226. func TestHTTPFsWalk(t *testing.T) {
  227. user := getTestUserWithHTTPFs(false)
  228. user.FsConfig.HTTPConfig.EqualityCheckMode = 1
  229. httpFs, err := user.GetFilesystem("")
  230. require.NoError(t, err)
  231. basePath := filepath.Join(os.TempDir(), "httpfs", user.FsConfig.HTTPConfig.Username)
  232. err = os.RemoveAll(basePath)
  233. assert.NoError(t, err)
  234. var walkedPaths []string
  235. err = httpFs.Walk("/", func(walkedPath string, _ fs.FileInfo, err error) error {
  236. if err != nil {
  237. return err
  238. }
  239. walkedPaths = append(walkedPaths, httpFs.GetRelativePath(walkedPath))
  240. return nil
  241. })
  242. require.NoError(t, err)
  243. require.Len(t, walkedPaths, 1)
  244. require.Contains(t, walkedPaths, "/")
  245. // now add some files/folders
  246. for i := 0; i < 10; i++ {
  247. err = os.WriteFile(filepath.Join(basePath, fmt.Sprintf("file%d", i)), nil, os.ModePerm)
  248. assert.NoError(t, err)
  249. err = os.Mkdir(filepath.Join(basePath, fmt.Sprintf("dir%d", i)), os.ModePerm)
  250. assert.NoError(t, err)
  251. for j := 0; j < 5; j++ {
  252. err = os.WriteFile(filepath.Join(basePath, fmt.Sprintf("dir%d", i), fmt.Sprintf("subfile%d", j)), nil, os.ModePerm)
  253. assert.NoError(t, err)
  254. }
  255. }
  256. walkedPaths = nil
  257. err = httpFs.Walk("/", func(walkedPath string, _ fs.FileInfo, err error) error {
  258. if err != nil {
  259. return err
  260. }
  261. walkedPaths = append(walkedPaths, httpFs.GetRelativePath(walkedPath))
  262. return nil
  263. })
  264. require.NoError(t, err)
  265. require.Len(t, walkedPaths, 71)
  266. require.Contains(t, walkedPaths, "/")
  267. for i := 0; i < 10; i++ {
  268. require.Contains(t, walkedPaths, path.Join("/", fmt.Sprintf("file%d", i)))
  269. require.Contains(t, walkedPaths, path.Join("/", fmt.Sprintf("dir%d", i)))
  270. for j := 0; j < 5; j++ {
  271. require.Contains(t, walkedPaths, path.Join("/", fmt.Sprintf("dir%d", i), fmt.Sprintf("subfile%d", j)))
  272. }
  273. }
  274. err = os.RemoveAll(basePath)
  275. assert.NoError(t, err)
  276. }
  277. func TestHTTPFsOverUNIXSocket(t *testing.T) {
  278. if runtime.GOOS == osWindows {
  279. t.Skip("UNIX domain sockets are not supported on Windows")
  280. }
  281. assert.Eventually(t, func() bool {
  282. _, err := os.Stat(httpFsSocketPath)
  283. return err == nil
  284. }, 1*time.Second, 50*time.Millisecond)
  285. usePubKey := true
  286. u := getTestUserWithHTTPFs(usePubKey)
  287. u.FsConfig.HTTPConfig.Endpoint = fmt.Sprintf("http://unix?socket_path=%s&api_prefix=%s",
  288. url.QueryEscape(httpFsSocketPath), url.QueryEscape("/api/v1"))
  289. user, _, err := httpdtest.AddUser(u, http.StatusCreated)
  290. assert.NoError(t, err)
  291. conn, client, err := getSftpClient(user, usePubKey)
  292. if assert.NoError(t, err) {
  293. defer conn.Close()
  294. defer client.Close()
  295. err = checkBasicSFTP(client)
  296. assert.NoError(t, err)
  297. testFilePath := filepath.Join(homeBasePath, testFileName)
  298. testFileSize := int64(65535)
  299. err = createTestFile(testFilePath, testFileSize)
  300. assert.NoError(t, err)
  301. err = sftpUploadFile(testFilePath, testFileName, testFileSize, client)
  302. assert.NoError(t, err)
  303. err = client.Remove(testFileName)
  304. assert.NoError(t, err)
  305. err = client.Mkdir(testFileName)
  306. assert.NoError(t, err)
  307. err = client.RemoveDirectory(testFileName)
  308. assert.NoError(t, err)
  309. err = os.Remove(testFilePath)
  310. assert.NoError(t, err)
  311. }
  312. _, err = httpdtest.RemoveUser(user, http.StatusOK)
  313. assert.NoError(t, err)
  314. err = os.RemoveAll(user.GetHomeDir())
  315. assert.NoError(t, err)
  316. }
  317. func getTestUserWithHTTPFs(usePubKey bool) dataprovider.User {
  318. u := getTestUser(usePubKey)
  319. u.FsConfig.Provider = sdk.HTTPFilesystemProvider
  320. u.FsConfig.HTTPConfig = vfs.HTTPFsConfig{
  321. BaseHTTPFsConfig: sdk.BaseHTTPFsConfig{
  322. Endpoint: fmt.Sprintf("http://127.0.0.1:%d/api/v1", httpFsPort),
  323. Username: defaultHTTPFsUsername,
  324. },
  325. }
  326. return u
  327. }
  328. func startHTTPFs() {
  329. if runtime.GOOS != osWindows {
  330. go func() {
  331. if err := httpdtest.StartTestHTTPFsOverUnixSocket(httpFsSocketPath); err != nil {
  332. logger.ErrorToConsole("could not start HTTPfs test server over UNIX socket: %v", err)
  333. os.Exit(1)
  334. }
  335. }()
  336. }
  337. go func() {
  338. if err := httpdtest.StartTestHTTPFs(httpFsPort, nil); err != nil {
  339. logger.ErrorToConsole("could not start HTTPfs test server: %v", err)
  340. os.Exit(1)
  341. }
  342. }()
  343. waitTCPListening(fmt.Sprintf(":%d", httpFsPort))
  344. }