api_shares.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418
  1. package httpd
  2. import (
  3. "context"
  4. "errors"
  5. "fmt"
  6. "net/http"
  7. "os"
  8. "path"
  9. "strings"
  10. "github.com/go-chi/render"
  11. "github.com/rs/xid"
  12. "github.com/drakkan/sftpgo/v2/common"
  13. "github.com/drakkan/sftpgo/v2/dataprovider"
  14. "github.com/drakkan/sftpgo/v2/logger"
  15. "github.com/drakkan/sftpgo/v2/util"
  16. )
  17. func getShares(w http.ResponseWriter, r *http.Request) {
  18. r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
  19. claims, err := getTokenClaims(r)
  20. if err != nil || claims.Username == "" {
  21. sendAPIResponse(w, r, err, "Invalid token claims", http.StatusBadRequest)
  22. return
  23. }
  24. limit, offset, order, err := getSearchFilters(w, r)
  25. if err != nil {
  26. return
  27. }
  28. shares, err := dataprovider.GetShares(limit, offset, order, claims.Username)
  29. if err != nil {
  30. sendAPIResponse(w, r, err, "", getRespStatus(err))
  31. return
  32. }
  33. render.JSON(w, r, shares)
  34. }
  35. func getShareByID(w http.ResponseWriter, r *http.Request) {
  36. r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
  37. claims, err := getTokenClaims(r)
  38. if err != nil || claims.Username == "" {
  39. sendAPIResponse(w, r, err, "Invalid token claims", http.StatusBadRequest)
  40. return
  41. }
  42. shareID := getURLParam(r, "id")
  43. share, err := dataprovider.ShareExists(shareID, claims.Username)
  44. if err != nil {
  45. sendAPIResponse(w, r, err, "", getRespStatus(err))
  46. return
  47. }
  48. share.HideConfidentialData()
  49. render.JSON(w, r, share)
  50. }
  51. func addShare(w http.ResponseWriter, r *http.Request) {
  52. r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
  53. claims, err := getTokenClaims(r)
  54. if err != nil || claims.Username == "" {
  55. sendAPIResponse(w, r, err, "Invalid token claims", http.StatusBadRequest)
  56. return
  57. }
  58. var share dataprovider.Share
  59. err = render.DecodeJSON(r.Body, &share)
  60. if err != nil {
  61. sendAPIResponse(w, r, err, "", http.StatusBadRequest)
  62. return
  63. }
  64. share.ID = 0
  65. share.ShareID = util.GenerateUniqueID()
  66. share.LastUseAt = 0
  67. share.Username = claims.Username
  68. if share.Name == "" {
  69. share.Name = share.ShareID
  70. }
  71. err = dataprovider.AddShare(&share, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr))
  72. if err != nil {
  73. sendAPIResponse(w, r, err, "", getRespStatus(err))
  74. return
  75. }
  76. w.Header().Add("Location", fmt.Sprintf("%v/%v", userSharesPath, share.ShareID))
  77. w.Header().Add("X-Object-ID", share.ShareID)
  78. sendAPIResponse(w, r, nil, "Share created", http.StatusCreated)
  79. }
  80. func updateShare(w http.ResponseWriter, r *http.Request) {
  81. r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
  82. claims, err := getTokenClaims(r)
  83. if err != nil || claims.Username == "" {
  84. sendAPIResponse(w, r, err, "Invalid token claims", http.StatusBadRequest)
  85. return
  86. }
  87. shareID := getURLParam(r, "id")
  88. share, err := dataprovider.ShareExists(shareID, claims.Username)
  89. if err != nil {
  90. sendAPIResponse(w, r, err, "", getRespStatus(err))
  91. return
  92. }
  93. oldPassword := share.Password
  94. err = render.DecodeJSON(r.Body, &share)
  95. if err != nil {
  96. sendAPIResponse(w, r, err, "", http.StatusBadRequest)
  97. return
  98. }
  99. share.ShareID = shareID
  100. share.Username = claims.Username
  101. if share.Password == redactedSecret {
  102. share.Password = oldPassword
  103. }
  104. if err := dataprovider.UpdateShare(&share, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr)); err != nil {
  105. sendAPIResponse(w, r, err, "", getRespStatus(err))
  106. return
  107. }
  108. sendAPIResponse(w, r, nil, "Share updated", http.StatusOK)
  109. }
  110. func deleteShare(w http.ResponseWriter, r *http.Request) {
  111. r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
  112. shareID := getURLParam(r, "id")
  113. claims, err := getTokenClaims(r)
  114. if err != nil || claims.Username == "" {
  115. sendAPIResponse(w, r, err, "Invalid token claims", http.StatusBadRequest)
  116. return
  117. }
  118. err = dataprovider.DeleteShare(shareID, claims.Username, util.GetIPFromRemoteAddress(r.RemoteAddr))
  119. if err != nil {
  120. sendAPIResponse(w, r, err, "", getRespStatus(err))
  121. return
  122. }
  123. sendAPIResponse(w, r, err, "Share deleted", http.StatusOK)
  124. }
  125. func readBrowsableShareContents(w http.ResponseWriter, r *http.Request) {
  126. r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
  127. share, connection, err := checkPublicShare(w, r, dataprovider.ShareScopeRead, false)
  128. if err != nil {
  129. return
  130. }
  131. if err := validateBrowsableShare(share, connection); err != nil {
  132. sendAPIResponse(w, r, err, "", getRespStatus(err))
  133. return
  134. }
  135. name, err := getBrowsableSharedPath(share, r)
  136. if err != nil {
  137. sendAPIResponse(w, r, err, "", getRespStatus(err))
  138. return
  139. }
  140. common.Connections.Add(connection)
  141. defer common.Connections.Remove(connection.GetID())
  142. contents, err := connection.ReadDir(name)
  143. if err != nil {
  144. sendAPIResponse(w, r, err, "Unable to get directory contents", getMappedStatusCode(err))
  145. return
  146. }
  147. renderAPIDirContents(w, r, contents, true)
  148. }
  149. func downloadBrowsableSharedFile(w http.ResponseWriter, r *http.Request) {
  150. r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
  151. share, connection, err := checkPublicShare(w, r, dataprovider.ShareScopeRead, false)
  152. if err != nil {
  153. return
  154. }
  155. if err := validateBrowsableShare(share, connection); err != nil {
  156. sendAPIResponse(w, r, err, "", getRespStatus(err))
  157. return
  158. }
  159. name, err := getBrowsableSharedPath(share, r)
  160. if err != nil {
  161. sendAPIResponse(w, r, err, "", getRespStatus(err))
  162. return
  163. }
  164. common.Connections.Add(connection)
  165. defer common.Connections.Remove(connection.GetID())
  166. info, err := connection.Stat(name, 1)
  167. if err != nil {
  168. sendAPIResponse(w, r, err, "Unable to stat the requested file", getMappedStatusCode(err))
  169. return
  170. }
  171. if info.IsDir() {
  172. sendAPIResponse(w, r, nil, fmt.Sprintf("Please set the path to a valid file, %#v is a directory", name),
  173. http.StatusBadRequest)
  174. return
  175. }
  176. inline := r.URL.Query().Get("inline") != ""
  177. dataprovider.UpdateShareLastUse(&share, 1) //nolint:errcheck
  178. if status, err := downloadFile(w, r, connection, name, info, inline, &share); err != nil {
  179. dataprovider.UpdateShareLastUse(&share, -1) //nolint:errcheck
  180. resp := apiResponse{
  181. Error: err.Error(),
  182. Message: http.StatusText(status),
  183. }
  184. ctx := r.Context()
  185. if status != 0 {
  186. ctx = context.WithValue(ctx, render.StatusCtxKey, status)
  187. }
  188. render.JSON(w, r.WithContext(ctx), resp)
  189. }
  190. }
  191. func downloadFromShare(w http.ResponseWriter, r *http.Request) {
  192. r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
  193. share, connection, err := checkPublicShare(w, r, dataprovider.ShareScopeRead, false)
  194. if err != nil {
  195. return
  196. }
  197. common.Connections.Add(connection)
  198. defer common.Connections.Remove(connection.GetID())
  199. compress := true
  200. var info os.FileInfo
  201. if len(share.Paths) == 1 && r.URL.Query().Get("compress") == "false" {
  202. info, err = connection.Stat(share.Paths[0], 1)
  203. if err != nil {
  204. sendAPIResponse(w, r, err, "", getRespStatus(err))
  205. return
  206. }
  207. if info.Mode().IsRegular() {
  208. compress = false
  209. }
  210. }
  211. dataprovider.UpdateShareLastUse(&share, 1) //nolint:errcheck
  212. if compress {
  213. transferQuota := connection.GetTransferQuota()
  214. if !transferQuota.HasDownloadSpace() {
  215. err = connection.GetReadQuotaExceededError()
  216. connection.Log(logger.LevelInfo, "denying share read due to quota limits")
  217. sendAPIResponse(w, r, err, "", getMappedStatusCode(err))
  218. }
  219. w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"share-%v.zip\"", share.ShareID))
  220. renderCompressedFiles(w, connection, "/", share.Paths, &share)
  221. return
  222. }
  223. if status, err := downloadFile(w, r, connection, share.Paths[0], info, false, &share); err != nil {
  224. dataprovider.UpdateShareLastUse(&share, -1) //nolint:errcheck
  225. resp := apiResponse{
  226. Error: err.Error(),
  227. Message: http.StatusText(status),
  228. }
  229. ctx := r.Context()
  230. if status != 0 {
  231. ctx = context.WithValue(ctx, render.StatusCtxKey, status)
  232. }
  233. render.JSON(w, r.WithContext(ctx), resp)
  234. }
  235. }
  236. func uploadFileToShare(w http.ResponseWriter, r *http.Request) {
  237. if maxUploadFileSize > 0 {
  238. r.Body = http.MaxBytesReader(w, r.Body, maxUploadFileSize)
  239. }
  240. name := getURLParam(r, "name")
  241. share, connection, err := checkPublicShare(w, r, dataprovider.ShareScopeWrite, false)
  242. if err != nil {
  243. return
  244. }
  245. filePath := path.Join(share.Paths[0], name)
  246. if path.Dir(filePath) != share.Paths[0] {
  247. sendAPIResponse(w, r, err, "Uploading outside the share is not allowed", http.StatusForbidden)
  248. return
  249. }
  250. dataprovider.UpdateShareLastUse(&share, 1) //nolint:errcheck
  251. common.Connections.Add(connection)
  252. defer common.Connections.Remove(connection.GetID())
  253. if err := doUploadFile(w, r, connection, filePath); err != nil {
  254. dataprovider.UpdateShareLastUse(&share, -1) //nolint:errcheck
  255. }
  256. }
  257. func uploadFilesToShare(w http.ResponseWriter, r *http.Request) {
  258. if maxUploadFileSize > 0 {
  259. r.Body = http.MaxBytesReader(w, r.Body, maxUploadFileSize)
  260. }
  261. share, connection, err := checkPublicShare(w, r, dataprovider.ShareScopeWrite, false)
  262. if err != nil {
  263. return
  264. }
  265. transferQuota := connection.GetTransferQuota()
  266. if !transferQuota.HasUploadSpace() {
  267. connection.Log(logger.LevelInfo, "denying file write due to transfer quota limits")
  268. sendAPIResponse(w, r, common.ErrQuotaExceeded, "Denying file write due to transfer quota limits",
  269. http.StatusRequestEntityTooLarge)
  270. return
  271. }
  272. common.Connections.Add(connection)
  273. defer common.Connections.Remove(connection.GetID())
  274. t := newThrottledReader(r.Body, connection.User.UploadBandwidth, connection)
  275. r.Body = t
  276. err = r.ParseMultipartForm(maxMultipartMem)
  277. if err != nil {
  278. connection.RemoveTransfer(t)
  279. sendAPIResponse(w, r, err, "Unable to parse multipart form", http.StatusBadRequest)
  280. return
  281. }
  282. connection.RemoveTransfer(t)
  283. defer r.MultipartForm.RemoveAll() //nolint:errcheck
  284. files := r.MultipartForm.File["filenames"]
  285. if len(files) == 0 {
  286. sendAPIResponse(w, r, nil, "No files uploaded!", http.StatusBadRequest)
  287. return
  288. }
  289. if share.MaxTokens > 0 {
  290. if len(files) > (share.MaxTokens - share.UsedTokens) {
  291. sendAPIResponse(w, r, nil, "Allowed usage exceeded", http.StatusBadRequest)
  292. return
  293. }
  294. }
  295. dataprovider.UpdateShareLastUse(&share, len(files)) //nolint:errcheck
  296. numUploads := doUploadFiles(w, r, connection, share.Paths[0], files)
  297. if numUploads != len(files) {
  298. dataprovider.UpdateShareLastUse(&share, numUploads-len(files)) //nolint:errcheck
  299. }
  300. }
  301. func checkPublicShare(w http.ResponseWriter, r *http.Request, shareShope dataprovider.ShareScope,
  302. isWebClient bool,
  303. ) (dataprovider.Share, *Connection, error) {
  304. renderError := func(err error, message string, statusCode int) {
  305. if isWebClient {
  306. renderClientMessagePage(w, r, "Unable to access the share", message, statusCode, err, "")
  307. } else {
  308. sendAPIResponse(w, r, err, message, statusCode)
  309. }
  310. }
  311. shareID := getURLParam(r, "id")
  312. share, err := dataprovider.ShareExists(shareID, "")
  313. if err != nil {
  314. statusCode := getRespStatus(err)
  315. if statusCode == http.StatusNotFound {
  316. err = errors.New("share does not exist")
  317. }
  318. renderError(err, "", statusCode)
  319. return share, nil, err
  320. }
  321. if share.Scope != shareShope {
  322. renderError(nil, "Invalid share scope", http.StatusForbidden)
  323. return share, nil, errors.New("invalid share scope")
  324. }
  325. ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
  326. ok, err := share.IsUsable(ipAddr)
  327. if !ok || err != nil {
  328. renderError(err, "", getRespStatus(err))
  329. return share, nil, err
  330. }
  331. if share.Password != "" {
  332. _, password, ok := r.BasicAuth()
  333. if !ok {
  334. w.Header().Set(common.HTTPAuthenticationHeader, basicRealm)
  335. renderError(dataprovider.ErrInvalidCredentials, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
  336. return share, nil, dataprovider.ErrInvalidCredentials
  337. }
  338. match, err := share.CheckPassword(password)
  339. if !match || err != nil {
  340. w.Header().Set(common.HTTPAuthenticationHeader, basicRealm)
  341. renderError(dataprovider.ErrInvalidCredentials, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
  342. return share, nil, dataprovider.ErrInvalidCredentials
  343. }
  344. }
  345. user, err := dataprovider.UserExists(share.Username)
  346. if err != nil {
  347. renderError(err, "", getRespStatus(err))
  348. return share, nil, err
  349. }
  350. connID := xid.New().String()
  351. connection := &Connection{
  352. BaseConnection: common.NewBaseConnection(connID, common.ProtocolHTTPShare, util.GetHTTPLocalAddress(r),
  353. r.RemoteAddr, user),
  354. request: r,
  355. }
  356. return share, connection, nil
  357. }
  358. func validateBrowsableShare(share dataprovider.Share, connection *Connection) error {
  359. if len(share.Paths) != 1 {
  360. return util.NewValidationError("A share with multiple paths is not browsable")
  361. }
  362. basePath := share.Paths[0]
  363. info, err := connection.Stat(basePath, 0)
  364. if err != nil {
  365. return fmt.Errorf("unable to check the share directory: %w", err)
  366. }
  367. if !info.IsDir() {
  368. return util.NewValidationError("The shared object is not a directory and so it is not browsable")
  369. }
  370. return nil
  371. }
  372. func getBrowsableSharedPath(share dataprovider.Share, r *http.Request) (string, error) {
  373. name := util.CleanPath(path.Join(share.Paths[0], r.URL.Query().Get("path")))
  374. if share.Paths[0] == "/" {
  375. return name, nil
  376. }
  377. if name != share.Paths[0] && !strings.HasPrefix(name, share.Paths[0]+"/") {
  378. return "", util.NewValidationError(fmt.Sprintf("Invalid path %#v", r.URL.Query().Get("path")))
  379. }
  380. return name, nil
  381. }