httpfsimpl.go 14 KB

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