httpfsimpl.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532
  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 httpdtest
  15. import (
  16. "context"
  17. "errors"
  18. "fmt"
  19. "io"
  20. "mime"
  21. "net"
  22. "net/http"
  23. "net/url"
  24. "os"
  25. "path/filepath"
  26. "strconv"
  27. "time"
  28. "github.com/go-chi/chi/v5"
  29. "github.com/go-chi/chi/v5/middleware"
  30. "github.com/go-chi/render"
  31. "github.com/shirou/gopsutil/v3/disk"
  32. "github.com/drakkan/sftpgo/v2/internal/util"
  33. )
  34. const (
  35. statPath = "/api/v1/stat"
  36. openPath = "/api/v1/open"
  37. createPath = "/api/v1/create"
  38. renamePath = "/api/v1/rename"
  39. removePath = "/api/v1/remove"
  40. mkdirPath = "/api/v1/mkdir"
  41. chmodPath = "/api/v1/chmod"
  42. chtimesPath = "/api/v1/chtimes"
  43. truncatePath = "/api/v1/truncate"
  44. readdirPath = "/api/v1/readdir"
  45. dirsizePath = "/api/v1/dirsize"
  46. mimetypePath = "/api/v1/mimetype"
  47. statvfsPath = "/api/v1/statvfs"
  48. )
  49. // StartTestHTTPFs starts a test HTTP service that implements httpfs
  50. // and listens on the specified port
  51. func StartTestHTTPFs(port int) error {
  52. fs := httpFsImpl{
  53. port: port,
  54. }
  55. return fs.Run()
  56. }
  57. // StartTestHTTPFsOverUnixSocket starts a test HTTP service that implements httpfs
  58. // and listens on the specified UNIX domain socket path
  59. func StartTestHTTPFsOverUnixSocket(socketPath string) error {
  60. fs := httpFsImpl{
  61. unixSocketPath: socketPath,
  62. }
  63. return fs.Run()
  64. }
  65. type httpFsImpl struct {
  66. router *chi.Mux
  67. basePath string
  68. port int
  69. unixSocketPath string
  70. }
  71. type apiResponse struct {
  72. Error string `json:"error,omitempty"`
  73. Message string `json:"message,omitempty"`
  74. }
  75. func (fs *httpFsImpl) sendAPIResponse(w http.ResponseWriter, r *http.Request, err error, message string, code int) {
  76. var errorString string
  77. if err != nil {
  78. errorString = err.Error()
  79. }
  80. resp := apiResponse{
  81. Error: errorString,
  82. Message: message,
  83. }
  84. ctx := context.WithValue(r.Context(), render.StatusCtxKey, code)
  85. render.JSON(w, r.WithContext(ctx), resp)
  86. }
  87. func (fs *httpFsImpl) getUsername(r *http.Request) (string, error) {
  88. username, _, ok := r.BasicAuth()
  89. if !ok || username == "" {
  90. return "", os.ErrPermission
  91. }
  92. rootPath := filepath.Join(fs.basePath, username)
  93. _, err := os.Stat(rootPath)
  94. if errors.Is(err, os.ErrNotExist) {
  95. err = os.MkdirAll(rootPath, os.ModePerm)
  96. if err != nil {
  97. return username, err
  98. }
  99. }
  100. return username, nil
  101. }
  102. func (fs *httpFsImpl) getRespStatus(err error) int {
  103. if errors.Is(err, os.ErrPermission) {
  104. return http.StatusForbidden
  105. }
  106. if errors.Is(err, os.ErrNotExist) {
  107. return http.StatusNotFound
  108. }
  109. return http.StatusInternalServerError
  110. }
  111. func (fs *httpFsImpl) stat(w http.ResponseWriter, r *http.Request) {
  112. username, err := fs.getUsername(r)
  113. if err != nil {
  114. fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
  115. return
  116. }
  117. name := getNameURLParam(r)
  118. fsPath := filepath.Join(fs.basePath, username, name)
  119. info, err := os.Stat(fsPath)
  120. if err != nil {
  121. fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
  122. return
  123. }
  124. render.JSON(w, r, getStatFromInfo(info))
  125. }
  126. func (fs *httpFsImpl) open(w http.ResponseWriter, r *http.Request) {
  127. username, err := fs.getUsername(r)
  128. if err != nil {
  129. fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
  130. return
  131. }
  132. var offset int64
  133. if r.URL.Query().Has("offset") {
  134. offset, err = strconv.ParseInt(r.URL.Query().Get("offset"), 10, 64)
  135. if err != nil {
  136. fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
  137. return
  138. }
  139. }
  140. name := getNameURLParam(r)
  141. fsPath := filepath.Join(fs.basePath, username, name)
  142. f, err := os.Open(fsPath)
  143. if err != nil {
  144. fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
  145. return
  146. }
  147. defer f.Close()
  148. if offset > 0 {
  149. _, err = f.Seek(offset, io.SeekStart)
  150. if err != nil {
  151. fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
  152. return
  153. }
  154. }
  155. ctype := mime.TypeByExtension(filepath.Ext(name))
  156. if ctype != "" {
  157. ctype = "application/octet-stream"
  158. }
  159. w.Header().Set("Content-Type", ctype)
  160. _, err = io.Copy(w, f)
  161. if err != nil {
  162. panic(http.ErrAbortHandler)
  163. }
  164. }
  165. func (fs *httpFsImpl) create(w http.ResponseWriter, r *http.Request) {
  166. username, err := fs.getUsername(r)
  167. if err != nil {
  168. fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
  169. return
  170. }
  171. flags := os.O_RDWR | os.O_CREATE | os.O_TRUNC
  172. if r.URL.Query().Has("flags") {
  173. openFlags, err := strconv.ParseInt(r.URL.Query().Get("flags"), 10, 32)
  174. if err != nil {
  175. fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
  176. return
  177. }
  178. if openFlags > 0 {
  179. flags = int(openFlags)
  180. }
  181. }
  182. name := getNameURLParam(r)
  183. fsPath := filepath.Join(fs.basePath, username, name)
  184. f, err := os.OpenFile(fsPath, flags, 0666)
  185. if err != nil {
  186. fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
  187. return
  188. }
  189. defer f.Close()
  190. _, err = io.Copy(f, r.Body)
  191. if err != nil {
  192. fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
  193. return
  194. }
  195. fs.sendAPIResponse(w, r, nil, "upload OK", http.StatusOK)
  196. }
  197. func (fs *httpFsImpl) rename(w http.ResponseWriter, r *http.Request) {
  198. username, err := fs.getUsername(r)
  199. if err != nil {
  200. fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
  201. return
  202. }
  203. target := r.URL.Query().Get("target")
  204. if target == "" {
  205. fs.sendAPIResponse(w, r, nil, "target path cannot be empty", http.StatusBadRequest)
  206. return
  207. }
  208. name := getNameURLParam(r)
  209. sourcePath := filepath.Join(fs.basePath, username, name)
  210. targetPath := filepath.Join(fs.basePath, username, target)
  211. err = os.Rename(sourcePath, targetPath)
  212. if err != nil {
  213. fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
  214. return
  215. }
  216. fs.sendAPIResponse(w, r, nil, "rename OK", http.StatusOK)
  217. }
  218. func (fs *httpFsImpl) remove(w http.ResponseWriter, r *http.Request) {
  219. username, err := fs.getUsername(r)
  220. if err != nil {
  221. fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
  222. return
  223. }
  224. name := getNameURLParam(r)
  225. fsPath := filepath.Join(fs.basePath, username, name)
  226. err = os.Remove(fsPath)
  227. if err != nil {
  228. fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
  229. return
  230. }
  231. fs.sendAPIResponse(w, r, nil, "remove OK", http.StatusOK)
  232. }
  233. func (fs *httpFsImpl) mkdir(w http.ResponseWriter, r *http.Request) {
  234. username, err := fs.getUsername(r)
  235. if err != nil {
  236. fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
  237. return
  238. }
  239. name := getNameURLParam(r)
  240. fsPath := filepath.Join(fs.basePath, username, name)
  241. err = os.Mkdir(fsPath, os.ModePerm)
  242. if err != nil {
  243. fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
  244. return
  245. }
  246. fs.sendAPIResponse(w, r, nil, "mkdir OK", http.StatusOK)
  247. }
  248. func (fs *httpFsImpl) chmod(w http.ResponseWriter, r *http.Request) {
  249. username, err := fs.getUsername(r)
  250. if err != nil {
  251. fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
  252. return
  253. }
  254. mode, err := strconv.ParseUint(r.URL.Query().Get("mode"), 10, 32)
  255. if err != nil {
  256. fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
  257. return
  258. }
  259. name := getNameURLParam(r)
  260. fsPath := filepath.Join(fs.basePath, username, name)
  261. err = os.Chmod(fsPath, os.FileMode(mode))
  262. if err != nil {
  263. fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
  264. return
  265. }
  266. fs.sendAPIResponse(w, r, nil, "chmod OK", http.StatusOK)
  267. }
  268. func (fs *httpFsImpl) chtimes(w http.ResponseWriter, r *http.Request) {
  269. username, err := fs.getUsername(r)
  270. if err != nil {
  271. fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
  272. return
  273. }
  274. atime, err := time.Parse(time.RFC3339, r.URL.Query().Get("access_time"))
  275. if err != nil {
  276. fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
  277. return
  278. }
  279. mtime, err := time.Parse(time.RFC3339, r.URL.Query().Get("modification_time"))
  280. if err != nil {
  281. fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
  282. return
  283. }
  284. name := getNameURLParam(r)
  285. fsPath := filepath.Join(fs.basePath, username, name)
  286. err = os.Chtimes(fsPath, atime, mtime)
  287. if err != nil {
  288. fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
  289. return
  290. }
  291. fs.sendAPIResponse(w, r, nil, "chtimes OK", http.StatusOK)
  292. }
  293. func (fs *httpFsImpl) truncate(w http.ResponseWriter, r *http.Request) {
  294. username, err := fs.getUsername(r)
  295. if err != nil {
  296. fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
  297. return
  298. }
  299. size, err := strconv.ParseInt(r.URL.Query().Get("size"), 10, 64)
  300. if err != nil {
  301. fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
  302. return
  303. }
  304. name := getNameURLParam(r)
  305. fsPath := filepath.Join(fs.basePath, username, name)
  306. err = os.Truncate(fsPath, size)
  307. if err != nil {
  308. fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
  309. return
  310. }
  311. fs.sendAPIResponse(w, r, nil, "chmod OK", http.StatusOK)
  312. }
  313. func (fs *httpFsImpl) readdir(w http.ResponseWriter, r *http.Request) {
  314. username, err := fs.getUsername(r)
  315. if err != nil {
  316. fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
  317. return
  318. }
  319. name := getNameURLParam(r)
  320. fsPath := filepath.Join(fs.basePath, username, name)
  321. f, err := os.Open(fsPath)
  322. if err != nil {
  323. fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
  324. return
  325. }
  326. list, err := f.Readdir(-1)
  327. f.Close()
  328. if err != nil {
  329. fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
  330. return
  331. }
  332. result := make([]map[string]any, 0, len(list))
  333. for _, fi := range list {
  334. result = append(result, getStatFromInfo(fi))
  335. }
  336. render.JSON(w, r, result)
  337. }
  338. func (fs *httpFsImpl) dirsize(w http.ResponseWriter, r *http.Request) {
  339. username, err := fs.getUsername(r)
  340. if err != nil {
  341. fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
  342. return
  343. }
  344. name := getNameURLParam(r)
  345. fsPath := filepath.Join(fs.basePath, username, name)
  346. info, err := os.Stat(fsPath)
  347. if err != nil {
  348. fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
  349. return
  350. }
  351. numFiles := 0
  352. size := int64(0)
  353. if info.IsDir() {
  354. err = filepath.Walk(fsPath, func(_ string, info os.FileInfo, err error) error {
  355. if err != nil {
  356. return err
  357. }
  358. if info != nil && info.Mode().IsRegular() {
  359. size += info.Size()
  360. numFiles++
  361. }
  362. return err
  363. })
  364. if err != nil {
  365. fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
  366. return
  367. }
  368. }
  369. render.JSON(w, r, map[string]any{
  370. "files": numFiles,
  371. "size": size,
  372. })
  373. }
  374. func (fs *httpFsImpl) mimetype(w http.ResponseWriter, r *http.Request) {
  375. username, err := fs.getUsername(r)
  376. if err != nil {
  377. fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
  378. return
  379. }
  380. name := getNameURLParam(r)
  381. fsPath := filepath.Join(fs.basePath, username, name)
  382. f, err := os.OpenFile(fsPath, os.O_RDONLY, 0)
  383. if err != nil {
  384. fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
  385. return
  386. }
  387. defer f.Close()
  388. var buf [512]byte
  389. n, err := io.ReadFull(f, buf[:])
  390. if err != nil && err != io.EOF && err != io.ErrUnexpectedEOF {
  391. fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
  392. return
  393. }
  394. ctype := http.DetectContentType(buf[:n])
  395. render.JSON(w, r, map[string]any{
  396. "mime": ctype,
  397. })
  398. }
  399. func (fs *httpFsImpl) statvfs(w http.ResponseWriter, r *http.Request) {
  400. username, err := fs.getUsername(r)
  401. if err != nil {
  402. fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
  403. return
  404. }
  405. name := getNameURLParam(r)
  406. fsPath := filepath.Join(fs.basePath, username, name)
  407. usage, err := disk.Usage(fsPath)
  408. if err != nil {
  409. fs.sendAPIResponse(w, r, err, "", fs.getRespStatus(err))
  410. return
  411. }
  412. // we assume block size = 4096
  413. bsize := uint64(4096)
  414. blocks := usage.Total / bsize
  415. bfree := usage.Free / bsize
  416. files := usage.InodesTotal
  417. ffree := usage.InodesFree
  418. if files == 0 {
  419. // these assumptions are wrong but still better than returning 0
  420. files = blocks / 4
  421. ffree = bfree / 4
  422. }
  423. render.JSON(w, r, map[string]any{
  424. "bsize": bsize,
  425. "frsize": bsize,
  426. "blocks": blocks,
  427. "bfree": bfree,
  428. "bavail": bfree,
  429. "files": files,
  430. "ffree": ffree,
  431. "favail": ffree,
  432. "namemax": 255,
  433. })
  434. }
  435. func (fs *httpFsImpl) configureRouter() {
  436. fs.router = chi.NewRouter()
  437. fs.router.Use(middleware.Recoverer)
  438. fs.router.Get(statPath+"/{name}", fs.stat)
  439. fs.router.Get(openPath+"/{name}", fs.open)
  440. fs.router.Post(createPath+"/{name}", fs.create)
  441. fs.router.Patch(renamePath+"/{name}", fs.rename)
  442. fs.router.Delete(removePath+"/{name}", fs.remove)
  443. fs.router.Post(mkdirPath+"/{name}", fs.mkdir)
  444. fs.router.Patch(chmodPath+"/{name}", fs.chmod)
  445. fs.router.Patch(chtimesPath+"/{name}", fs.chtimes)
  446. fs.router.Patch(truncatePath+"/{name}", fs.truncate)
  447. fs.router.Get(readdirPath+"/{name}", fs.readdir)
  448. fs.router.Get(dirsizePath+"/{name}", fs.dirsize)
  449. fs.router.Get(mimetypePath+"/{name}", fs.mimetype)
  450. fs.router.Get(statvfsPath+"/{name}", fs.statvfs)
  451. }
  452. func (fs *httpFsImpl) Run() error {
  453. fs.basePath = filepath.Join(os.TempDir(), "httpfs")
  454. if err := os.RemoveAll(fs.basePath); err != nil {
  455. return err
  456. }
  457. if err := os.MkdirAll(fs.basePath, os.ModePerm); err != nil {
  458. return err
  459. }
  460. fs.configureRouter()
  461. httpServer := http.Server{
  462. Addr: fmt.Sprintf(":%d", fs.port),
  463. Handler: fs.router,
  464. ReadTimeout: 60 * time.Second,
  465. WriteTimeout: 60 * time.Second,
  466. IdleTimeout: 120 * time.Second,
  467. MaxHeaderBytes: 1 << 16, // 64KB
  468. }
  469. if fs.unixSocketPath == "" {
  470. return httpServer.ListenAndServe()
  471. }
  472. err := os.Remove(fs.unixSocketPath)
  473. if err != nil && !os.IsNotExist(err) {
  474. return err
  475. }
  476. listener, err := net.Listen("unix", fs.unixSocketPath)
  477. if err != nil {
  478. return err
  479. }
  480. return httpServer.Serve(listener)
  481. }
  482. func getStatFromInfo(info os.FileInfo) map[string]any {
  483. return map[string]any{
  484. "name": info.Name(),
  485. "size": info.Size(),
  486. "mode": info.Mode(),
  487. "last_modified": info.ModTime(),
  488. }
  489. }
  490. func getNameURLParam(r *http.Request) string {
  491. v := chi.URLParam(r, "name")
  492. unescaped, err := url.PathUnescape(v)
  493. if err != nil {
  494. return util.CleanPath(v)
  495. }
  496. return util.CleanPath(unescaped)
  497. }