webclient.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476
  1. package httpd
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "html/template"
  6. "net/http"
  7. "net/url"
  8. "os"
  9. "path"
  10. "path/filepath"
  11. "time"
  12. "github.com/go-chi/render"
  13. "github.com/rs/xid"
  14. "github.com/drakkan/sftpgo/v2/common"
  15. "github.com/drakkan/sftpgo/v2/dataprovider"
  16. "github.com/drakkan/sftpgo/v2/utils"
  17. "github.com/drakkan/sftpgo/v2/version"
  18. "github.com/drakkan/sftpgo/v2/vfs"
  19. )
  20. const (
  21. templateClientDir = "webclient"
  22. templateClientBase = "base.html"
  23. templateClientLogin = "login.html"
  24. templateClientFiles = "files.html"
  25. templateClientMessage = "message.html"
  26. templateClientCredentials = "credentials.html"
  27. pageClientFilesTitle = "My Files"
  28. pageClientCredentialsTitle = "Credentials"
  29. )
  30. // condResult is the result of an HTTP request precondition check.
  31. // See https://tools.ietf.org/html/rfc7232 section 3.
  32. type condResult int
  33. const (
  34. condNone condResult = iota
  35. condTrue
  36. condFalse
  37. )
  38. var (
  39. clientTemplates = make(map[string]*template.Template)
  40. unixEpochTime = time.Unix(0, 0)
  41. )
  42. // isZeroTime reports whether t is obviously unspecified (either zero or Unix()=0).
  43. func isZeroTime(t time.Time) bool {
  44. return t.IsZero() || t.Equal(unixEpochTime)
  45. }
  46. type baseClientPage struct {
  47. Title string
  48. CurrentURL string
  49. FilesURL string
  50. CredentialsURL string
  51. StaticURL string
  52. LogoutURL string
  53. FilesTitle string
  54. CredentialsTitle string
  55. Version string
  56. CSRFToken string
  57. LoggedUser *dataprovider.User
  58. }
  59. type dirMapping struct {
  60. DirName string
  61. Href string
  62. }
  63. type filesPage struct {
  64. baseClientPage
  65. CurrentDir string
  66. ReadDirURL string
  67. DownloadURL string
  68. Error string
  69. Paths []dirMapping
  70. }
  71. type clientMessagePage struct {
  72. baseClientPage
  73. Error string
  74. Success string
  75. }
  76. type credentialsPage struct {
  77. baseClientPage
  78. PublicKeys []string
  79. ChangePwdURL string
  80. ManageKeysURL string
  81. PwdError string
  82. KeyError string
  83. }
  84. func getFileObjectURL(baseDir, name string) string {
  85. return fmt.Sprintf("%v?path=%v", webClientFilesPath, url.QueryEscape(path.Join(baseDir, name)))
  86. }
  87. func getFileObjectModTime(t time.Time) string {
  88. if isZeroTime(t) {
  89. return ""
  90. }
  91. return t.Format("2006-01-02 15:04")
  92. }
  93. func loadClientTemplates(templatesPath string) {
  94. filesPaths := []string{
  95. filepath.Join(templatesPath, templateClientDir, templateClientBase),
  96. filepath.Join(templatesPath, templateClientDir, templateClientFiles),
  97. }
  98. credentialsPaths := []string{
  99. filepath.Join(templatesPath, templateClientDir, templateClientBase),
  100. filepath.Join(templatesPath, templateClientDir, templateClientCredentials),
  101. }
  102. loginPath := []string{
  103. filepath.Join(templatesPath, templateClientDir, templateClientLogin),
  104. }
  105. messagePath := []string{
  106. filepath.Join(templatesPath, templateClientDir, templateClientBase),
  107. filepath.Join(templatesPath, templateClientDir, templateClientMessage),
  108. }
  109. filesTmpl := utils.LoadTemplate(nil, filesPaths...)
  110. credentialsTmpl := utils.LoadTemplate(nil, credentialsPaths...)
  111. loginTmpl := utils.LoadTemplate(nil, loginPath...)
  112. messageTmpl := utils.LoadTemplate(nil, messagePath...)
  113. clientTemplates[templateClientFiles] = filesTmpl
  114. clientTemplates[templateClientCredentials] = credentialsTmpl
  115. clientTemplates[templateClientLogin] = loginTmpl
  116. clientTemplates[templateClientMessage] = messageTmpl
  117. }
  118. func getBaseClientPageData(title, currentURL string, r *http.Request) baseClientPage {
  119. var csrfToken string
  120. if currentURL != "" {
  121. csrfToken = createCSRFToken()
  122. }
  123. v := version.Get()
  124. return baseClientPage{
  125. Title: title,
  126. CurrentURL: currentURL,
  127. FilesURL: webClientFilesPath,
  128. CredentialsURL: webClientCredentialsPath,
  129. StaticURL: webStaticFilesPath,
  130. LogoutURL: webClientLogoutPath,
  131. FilesTitle: pageClientFilesTitle,
  132. CredentialsTitle: pageClientCredentialsTitle,
  133. Version: fmt.Sprintf("%v-%v", v.Version, v.CommitHash),
  134. CSRFToken: csrfToken,
  135. LoggedUser: getUserFromToken(r),
  136. }
  137. }
  138. func renderClientTemplate(w http.ResponseWriter, tmplName string, data interface{}) {
  139. err := clientTemplates[tmplName].ExecuteTemplate(w, tmplName, data)
  140. if err != nil {
  141. http.Error(w, err.Error(), http.StatusInternalServerError)
  142. }
  143. }
  144. func renderClientLoginPage(w http.ResponseWriter, error string) {
  145. data := loginPage{
  146. CurrentURL: webClientLoginPath,
  147. Version: version.Get().Version,
  148. Error: error,
  149. CSRFToken: createCSRFToken(),
  150. StaticURL: webStaticFilesPath,
  151. }
  152. renderClientTemplate(w, templateClientLogin, data)
  153. }
  154. func renderClientMessagePage(w http.ResponseWriter, r *http.Request, title, body string, statusCode int, err error, message string) {
  155. var errorString string
  156. if body != "" {
  157. errorString = body + " "
  158. }
  159. if err != nil {
  160. errorString += err.Error()
  161. }
  162. data := clientMessagePage{
  163. baseClientPage: getBaseClientPageData(title, "", r),
  164. Error: errorString,
  165. Success: message,
  166. }
  167. w.WriteHeader(statusCode)
  168. renderClientTemplate(w, templateClientMessage, data)
  169. }
  170. func renderClientInternalServerErrorPage(w http.ResponseWriter, r *http.Request, err error) {
  171. renderClientMessagePage(w, r, page500Title, page500Body, http.StatusInternalServerError, err, "")
  172. }
  173. func renderClientBadRequestPage(w http.ResponseWriter, r *http.Request, err error) {
  174. renderClientMessagePage(w, r, page400Title, "", http.StatusBadRequest, err, "")
  175. }
  176. func renderClientForbiddenPage(w http.ResponseWriter, r *http.Request, body string) {
  177. renderClientMessagePage(w, r, page403Title, "", http.StatusForbidden, nil, body)
  178. }
  179. func renderClientNotFoundPage(w http.ResponseWriter, r *http.Request, err error) {
  180. renderClientMessagePage(w, r, page404Title, page404Body, http.StatusNotFound, err, "")
  181. }
  182. func renderFilesPage(w http.ResponseWriter, r *http.Request, dirName, error string) {
  183. data := filesPage{
  184. baseClientPage: getBaseClientPageData(pageClientFilesTitle, webClientFilesPath, r),
  185. Error: error,
  186. CurrentDir: url.QueryEscape(dirName),
  187. DownloadURL: webClientDownloadZipPath,
  188. ReadDirURL: webClientDirContentsPath,
  189. }
  190. paths := []dirMapping{}
  191. if dirName != "/" {
  192. paths = append(paths, dirMapping{
  193. DirName: path.Base(dirName),
  194. Href: "",
  195. })
  196. for {
  197. dirName = path.Dir(dirName)
  198. if dirName == "/" || dirName == "." {
  199. break
  200. }
  201. paths = append([]dirMapping{{
  202. DirName: path.Base(dirName),
  203. Href: getFileObjectURL("/", dirName)},
  204. }, paths...)
  205. }
  206. }
  207. data.Paths = paths
  208. renderClientTemplate(w, templateClientFiles, data)
  209. }
  210. func renderCredentialsPage(w http.ResponseWriter, r *http.Request, pwdError string, keyError string) {
  211. data := credentialsPage{
  212. baseClientPage: getBaseClientPageData(pageClientCredentialsTitle, webClientCredentialsPath, r),
  213. ChangePwdURL: webChangeClientPwdPath,
  214. ManageKeysURL: webChangeClientKeysPath,
  215. PwdError: pwdError,
  216. KeyError: keyError,
  217. }
  218. user, err := dataprovider.UserExists(data.LoggedUser.Username)
  219. if err != nil {
  220. renderClientInternalServerErrorPage(w, r, err)
  221. }
  222. data.PublicKeys = user.PublicKeys
  223. renderClientTemplate(w, templateClientCredentials, data)
  224. }
  225. func handleClientWebLogin(w http.ResponseWriter, r *http.Request) {
  226. renderClientLoginPage(w, "")
  227. }
  228. func handleWebClientLogout(w http.ResponseWriter, r *http.Request) {
  229. c := jwtTokenClaims{}
  230. c.removeCookie(w, r, webBaseClientPath)
  231. http.Redirect(w, r, webClientLoginPath, http.StatusFound)
  232. }
  233. func handleWebClientDownloadZip(w http.ResponseWriter, r *http.Request) {
  234. claims, err := getTokenClaims(r)
  235. if err != nil || claims.Username == "" {
  236. renderClientMessagePage(w, r, "Invalid token claims", "", http.StatusForbidden, nil, "")
  237. return
  238. }
  239. user, err := dataprovider.UserExists(claims.Username)
  240. if err != nil {
  241. renderClientMessagePage(w, r, "Unable to retrieve your user", "", getRespStatus(err), nil, "")
  242. return
  243. }
  244. connID := xid.New().String()
  245. connectionID := fmt.Sprintf("%v_%v", common.ProtocolHTTP, connID)
  246. if err := checkHTTPClientUser(&user, r, connectionID); err != nil {
  247. renderClientForbiddenPage(w, r, err.Error())
  248. return
  249. }
  250. connection := &Connection{
  251. BaseConnection: common.NewBaseConnection(connID, common.ProtocolHTTP, r.RemoteAddr, user),
  252. request: r,
  253. }
  254. common.Connections.Add(connection)
  255. defer common.Connections.Remove(connection.GetID())
  256. name := "/"
  257. if _, ok := r.URL.Query()["path"]; ok {
  258. name = utils.CleanPath(r.URL.Query().Get("path"))
  259. }
  260. files := r.URL.Query().Get("files")
  261. var filesList []string
  262. err = json.Unmarshal([]byte(files), &filesList)
  263. if err != nil {
  264. renderClientMessagePage(w, r, "Unable to get files list", "", http.StatusInternalServerError, err, "")
  265. return
  266. }
  267. w.Header().Set("Content-Disposition", "attachment; filename=\"sftpgo-download.zip\"")
  268. renderCompressedFiles(w, connection, name, filesList)
  269. }
  270. func handleClientGetDirContents(w http.ResponseWriter, r *http.Request) {
  271. claims, err := getTokenClaims(r)
  272. if err != nil || claims.Username == "" {
  273. sendAPIResponse(w, r, nil, "invalid token claims", http.StatusForbidden)
  274. return
  275. }
  276. user, err := dataprovider.UserExists(claims.Username)
  277. if err != nil {
  278. sendAPIResponse(w, r, nil, "Unable to retrieve your user", getRespStatus(err))
  279. return
  280. }
  281. connID := xid.New().String()
  282. connectionID := fmt.Sprintf("%v_%v", common.ProtocolHTTP, connID)
  283. if err := checkHTTPClientUser(&user, r, connectionID); err != nil {
  284. sendAPIResponse(w, r, err, http.StatusText(http.StatusForbidden), http.StatusForbidden)
  285. return
  286. }
  287. connection := &Connection{
  288. BaseConnection: common.NewBaseConnection(connID, common.ProtocolHTTP, r.RemoteAddr, user),
  289. request: r,
  290. }
  291. common.Connections.Add(connection)
  292. defer common.Connections.Remove(connection.GetID())
  293. name := "/"
  294. if _, ok := r.URL.Query()["path"]; ok {
  295. name = utils.CleanPath(r.URL.Query().Get("path"))
  296. }
  297. contents, err := connection.ReadDir(name)
  298. if err != nil {
  299. sendAPIResponse(w, r, err, "Unable to get directory contents", getMappedStatusCode(err))
  300. return
  301. }
  302. results := make([]map[string]string, 0, len(contents))
  303. for _, info := range contents {
  304. res := make(map[string]string)
  305. if info.IsDir() {
  306. res["type"] = "1"
  307. res["size"] = ""
  308. } else {
  309. res["type"] = "2"
  310. if info.Mode()&os.ModeSymlink != 0 {
  311. res["size"] = ""
  312. } else {
  313. res["size"] = utils.ByteCountIEC(info.Size())
  314. }
  315. }
  316. res["name"] = info.Name()
  317. res["last_modified"] = getFileObjectModTime(info.ModTime())
  318. res["url"] = getFileObjectURL(name, info.Name())
  319. results = append(results, res)
  320. }
  321. render.JSON(w, r, results)
  322. }
  323. func handleClientGetFiles(w http.ResponseWriter, r *http.Request) {
  324. claims, err := getTokenClaims(r)
  325. if err != nil || claims.Username == "" {
  326. renderClientForbiddenPage(w, r, "Invalid token claims")
  327. return
  328. }
  329. user, err := dataprovider.UserExists(claims.Username)
  330. if err != nil {
  331. renderClientMessagePage(w, r, "Unable to retrieve your user", "", getRespStatus(err), nil, "")
  332. return
  333. }
  334. connID := xid.New().String()
  335. connectionID := fmt.Sprintf("%v_%v", common.ProtocolHTTP, connID)
  336. if err := checkHTTPClientUser(&user, r, connectionID); err != nil {
  337. renderClientForbiddenPage(w, r, err.Error())
  338. return
  339. }
  340. connection := &Connection{
  341. BaseConnection: common.NewBaseConnection(connID, common.ProtocolHTTP, r.RemoteAddr, user),
  342. request: r,
  343. }
  344. common.Connections.Add(connection)
  345. defer common.Connections.Remove(connection.GetID())
  346. name := "/"
  347. if _, ok := r.URL.Query()["path"]; ok {
  348. name = utils.CleanPath(r.URL.Query().Get("path"))
  349. }
  350. var info os.FileInfo
  351. if name == "/" {
  352. info = vfs.NewFileInfo(name, true, 0, time.Now(), false)
  353. } else {
  354. info, err = connection.Stat(name, 0)
  355. }
  356. if err != nil {
  357. renderFilesPage(w, r, path.Dir(name), fmt.Sprintf("unable to stat file %#v: %v", name, err))
  358. return
  359. }
  360. if info.IsDir() {
  361. renderFilesPage(w, r, name, "")
  362. return
  363. }
  364. if status, err := downloadFile(w, r, connection, name, info); err != nil && status != 0 {
  365. if status > 0 {
  366. if status == http.StatusRequestedRangeNotSatisfiable {
  367. renderClientMessagePage(w, r, http.StatusText(status), "", status, err, "")
  368. return
  369. }
  370. renderFilesPage(w, r, path.Dir(name), err.Error())
  371. }
  372. }
  373. }
  374. func handleClientGetCredentials(w http.ResponseWriter, r *http.Request) {
  375. renderCredentialsPage(w, r, "", "")
  376. }
  377. func handleWebClientChangePwdPost(w http.ResponseWriter, r *http.Request) {
  378. r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
  379. err := r.ParseForm()
  380. if err != nil {
  381. renderCredentialsPage(w, r, err.Error(), "")
  382. return
  383. }
  384. if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil {
  385. renderClientForbiddenPage(w, r, err.Error())
  386. return
  387. }
  388. err = doChangeUserPassword(r, r.Form.Get("current_password"), r.Form.Get("new_password1"),
  389. r.Form.Get("new_password2"))
  390. if err != nil {
  391. renderCredentialsPage(w, r, err.Error(), "")
  392. return
  393. }
  394. handleWebClientLogout(w, r)
  395. }
  396. func handleWebClientManageKeysPost(w http.ResponseWriter, r *http.Request) {
  397. r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
  398. err := r.ParseForm()
  399. if err != nil {
  400. renderCredentialsPage(w, r, "", err.Error())
  401. return
  402. }
  403. if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil {
  404. renderClientForbiddenPage(w, r, err.Error())
  405. return
  406. }
  407. claims, err := getTokenClaims(r)
  408. if err != nil || claims.Username == "" {
  409. renderCredentialsPage(w, r, "", "Invalid token claims")
  410. return
  411. }
  412. user, err := dataprovider.UserExists(claims.Username)
  413. if err != nil {
  414. renderCredentialsPage(w, r, "", err.Error())
  415. return
  416. }
  417. user.PublicKeys = r.Form["public_keys"]
  418. err = dataprovider.UpdateUser(&user)
  419. if err != nil {
  420. renderCredentialsPage(w, r, "", err.Error())
  421. return
  422. }
  423. renderClientMessagePage(w, r, "Public keys updated", "", http.StatusOK, nil, "Your public keys has been successfully updated")
  424. }