web.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560
  1. package httpd
  2. import (
  3. "encoding/base64"
  4. "errors"
  5. "fmt"
  6. "html/template"
  7. "io/ioutil"
  8. "net/http"
  9. "path"
  10. "path/filepath"
  11. "strconv"
  12. "strings"
  13. "time"
  14. "github.com/go-chi/chi"
  15. "github.com/drakkan/sftpgo/dataprovider"
  16. "github.com/drakkan/sftpgo/sftpd"
  17. "github.com/drakkan/sftpgo/utils"
  18. "github.com/drakkan/sftpgo/vfs"
  19. )
  20. const (
  21. templateBase = "base.html"
  22. templateUsers = "users.html"
  23. templateUser = "user.html"
  24. templateConnections = "connections.html"
  25. templateMessage = "message.html"
  26. pageUsersTitle = "Users"
  27. pageConnectionsTitle = "Connections"
  28. page400Title = "Bad request"
  29. page404Title = "Not found"
  30. page404Body = "The page you are looking for does not exist."
  31. page500Title = "Internal Server Error"
  32. page500Body = "The server is unable to fulfill your request."
  33. defaultUsersQueryLimit = 500
  34. webDateTimeFormat = "2006-01-02 15:04:05" // YYYY-MM-DD HH:MM:SS
  35. )
  36. var (
  37. templates = make(map[string]*template.Template)
  38. )
  39. type basePage struct {
  40. Title string
  41. CurrentURL string
  42. UsersURL string
  43. UserURL string
  44. APIUserURL string
  45. APIConnectionsURL string
  46. APIQuotaScanURL string
  47. ConnectionsURL string
  48. UsersTitle string
  49. ConnectionsTitle string
  50. Version string
  51. }
  52. type usersPage struct {
  53. basePage
  54. Users []dataprovider.User
  55. }
  56. type connectionsPage struct {
  57. basePage
  58. Connections []sftpd.ConnectionStatus
  59. }
  60. type userPage struct {
  61. basePage
  62. IsAdd bool
  63. User dataprovider.User
  64. RootPerms []string
  65. Error string
  66. ValidPerms []string
  67. ValidSSHLoginMethods []string
  68. RootDirPerms []string
  69. }
  70. type messagePage struct {
  71. basePage
  72. Error string
  73. Success string
  74. }
  75. func loadTemplates(templatesPath string) {
  76. usersPaths := []string{
  77. filepath.Join(templatesPath, templateBase),
  78. filepath.Join(templatesPath, templateUsers),
  79. }
  80. userPaths := []string{
  81. filepath.Join(templatesPath, templateBase),
  82. filepath.Join(templatesPath, templateUser),
  83. }
  84. connectionsPaths := []string{
  85. filepath.Join(templatesPath, templateBase),
  86. filepath.Join(templatesPath, templateConnections),
  87. }
  88. messagePath := []string{
  89. filepath.Join(templatesPath, templateBase),
  90. filepath.Join(templatesPath, templateMessage),
  91. }
  92. usersTmpl := utils.LoadTemplate(template.ParseFiles(usersPaths...))
  93. userTmpl := utils.LoadTemplate(template.ParseFiles(userPaths...))
  94. connectionsTmpl := utils.LoadTemplate(template.ParseFiles(connectionsPaths...))
  95. messageTmpl := utils.LoadTemplate(template.ParseFiles(messagePath...))
  96. templates[templateUsers] = usersTmpl
  97. templates[templateUser] = userTmpl
  98. templates[templateConnections] = connectionsTmpl
  99. templates[templateMessage] = messageTmpl
  100. }
  101. func getBasePageData(title, currentURL string) basePage {
  102. version := utils.GetAppVersion()
  103. return basePage{
  104. Title: title,
  105. CurrentURL: currentURL,
  106. UsersURL: webUsersPath,
  107. UserURL: webUserPath,
  108. APIUserURL: userPath,
  109. APIConnectionsURL: activeConnectionsPath,
  110. APIQuotaScanURL: quotaScanPath,
  111. ConnectionsURL: webConnectionsPath,
  112. UsersTitle: pageUsersTitle,
  113. ConnectionsTitle: pageConnectionsTitle,
  114. Version: version.GetVersionAsString(),
  115. }
  116. }
  117. func renderTemplate(w http.ResponseWriter, tmplName string, data interface{}) {
  118. err := templates[tmplName].ExecuteTemplate(w, tmplName, data)
  119. if err != nil {
  120. http.Error(w, err.Error(), http.StatusInternalServerError)
  121. }
  122. }
  123. func renderMessagePage(w http.ResponseWriter, title, body string, statusCode int, err error, message string) {
  124. var errorString string
  125. if len(body) > 0 {
  126. errorString = body + " "
  127. }
  128. if err != nil {
  129. errorString += err.Error()
  130. }
  131. data := messagePage{
  132. basePage: getBasePageData(title, ""),
  133. Error: errorString,
  134. Success: message,
  135. }
  136. w.WriteHeader(statusCode)
  137. renderTemplate(w, templateMessage, data)
  138. }
  139. func renderInternalServerErrorPage(w http.ResponseWriter, err error) {
  140. renderMessagePage(w, page500Title, page500Body, http.StatusInternalServerError, err, "")
  141. }
  142. func renderBadRequestPage(w http.ResponseWriter, err error) {
  143. renderMessagePage(w, page400Title, "", http.StatusBadRequest, err, "")
  144. }
  145. func renderNotFoundPage(w http.ResponseWriter, err error) {
  146. renderMessagePage(w, page404Title, page404Body, http.StatusNotFound, err, "")
  147. }
  148. func renderAddUserPage(w http.ResponseWriter, user dataprovider.User, error string) {
  149. data := userPage{
  150. basePage: getBasePageData("Add a new user", webUserPath),
  151. IsAdd: true,
  152. Error: error,
  153. User: user,
  154. ValidPerms: dataprovider.ValidPerms,
  155. ValidSSHLoginMethods: dataprovider.ValidSSHLoginMethods,
  156. RootDirPerms: user.GetPermissionsForPath("/"),
  157. }
  158. renderTemplate(w, templateUser, data)
  159. }
  160. func renderUpdateUserPage(w http.ResponseWriter, user dataprovider.User, error string) {
  161. data := userPage{
  162. basePage: getBasePageData("Update user", fmt.Sprintf("%v/%v", webUserPath, user.ID)),
  163. IsAdd: false,
  164. Error: error,
  165. User: user,
  166. ValidPerms: dataprovider.ValidPerms,
  167. ValidSSHLoginMethods: dataprovider.ValidSSHLoginMethods,
  168. RootDirPerms: user.GetPermissionsForPath("/"),
  169. }
  170. renderTemplate(w, templateUser, data)
  171. }
  172. func getVirtualFoldersFromPostFields(r *http.Request) []vfs.VirtualFolder {
  173. var virtualFolders []vfs.VirtualFolder
  174. formValue := r.Form.Get("virtual_folders")
  175. for _, cleaned := range getSliceFromDelimitedValues(formValue, "\n") {
  176. if strings.Contains(cleaned, "::") {
  177. mapping := strings.Split(cleaned, "::")
  178. if len(mapping) > 1 {
  179. vfolder := vfs.VirtualFolder{
  180. VirtualPath: strings.TrimSpace(mapping[0]),
  181. MappedPath: strings.TrimSpace(mapping[1]),
  182. }
  183. if len(mapping) > 2 {
  184. excludeFromQuota, err := strconv.Atoi(strings.TrimSpace(mapping[2]))
  185. if err == nil {
  186. vfolder.ExcludeFromQuota = (excludeFromQuota > 0)
  187. }
  188. }
  189. virtualFolders = append(virtualFolders, vfolder)
  190. }
  191. }
  192. }
  193. return virtualFolders
  194. }
  195. func getUserPermissionsFromPostFields(r *http.Request) map[string][]string {
  196. permissions := make(map[string][]string)
  197. permissions["/"] = r.Form["permissions"]
  198. subDirsPermsValue := r.Form.Get("sub_dirs_permissions")
  199. for _, cleaned := range getSliceFromDelimitedValues(subDirsPermsValue, "\n") {
  200. if strings.Contains(cleaned, "::") {
  201. dirPerms := strings.Split(cleaned, "::")
  202. if len(dirPerms) > 1 {
  203. dir := dirPerms[0]
  204. dir = strings.TrimSpace(dir)
  205. perms := []string{}
  206. for _, p := range strings.Split(dirPerms[1], ",") {
  207. cleanedPerm := strings.TrimSpace(p)
  208. if len(cleanedPerm) > 0 {
  209. perms = append(perms, cleanedPerm)
  210. }
  211. }
  212. if len(dir) > 0 {
  213. permissions[dir] = perms
  214. }
  215. }
  216. }
  217. }
  218. return permissions
  219. }
  220. func getSliceFromDelimitedValues(values, delimiter string) []string {
  221. result := []string{}
  222. for _, v := range strings.Split(values, delimiter) {
  223. cleaned := strings.TrimSpace(v)
  224. if len(cleaned) > 0 {
  225. result = append(result, cleaned)
  226. }
  227. }
  228. return result
  229. }
  230. func getFileExtensionsFromPostField(value string, extesionsType int) []dataprovider.ExtensionsFilter {
  231. var result []dataprovider.ExtensionsFilter
  232. for _, cleaned := range getSliceFromDelimitedValues(value, "\n") {
  233. if strings.Contains(cleaned, "::") {
  234. dirExts := strings.Split(cleaned, "::")
  235. if len(dirExts) > 1 {
  236. dir := dirExts[0]
  237. dir = strings.TrimSpace(dir)
  238. exts := []string{}
  239. for _, e := range strings.Split(dirExts[1], ",") {
  240. cleanedExt := strings.TrimSpace(e)
  241. if len(cleanedExt) > 0 {
  242. exts = append(exts, cleanedExt)
  243. }
  244. }
  245. if len(dir) > 0 {
  246. filter := dataprovider.ExtensionsFilter{
  247. Path: dir,
  248. }
  249. if extesionsType == 1 {
  250. filter.AllowedExtensions = exts
  251. filter.DeniedExtensions = []string{}
  252. } else {
  253. filter.DeniedExtensions = exts
  254. filter.AllowedExtensions = []string{}
  255. }
  256. result = append(result, filter)
  257. }
  258. }
  259. }
  260. }
  261. return result
  262. }
  263. func getFiltersFromUserPostFields(r *http.Request) dataprovider.UserFilters {
  264. var filters dataprovider.UserFilters
  265. filters.AllowedIP = getSliceFromDelimitedValues(r.Form.Get("allowed_ip"), ",")
  266. filters.DeniedIP = getSliceFromDelimitedValues(r.Form.Get("denied_ip"), ",")
  267. filters.DeniedLoginMethods = r.Form["ssh_login_methods"]
  268. allowedExtensions := getFileExtensionsFromPostField(r.Form.Get("allowed_extensions"), 1)
  269. deniedExtensions := getFileExtensionsFromPostField(r.Form.Get("denied_extensions"), 2)
  270. extensions := []dataprovider.ExtensionsFilter{}
  271. if len(allowedExtensions) > 0 && len(deniedExtensions) > 0 {
  272. for _, allowed := range allowedExtensions {
  273. for _, denied := range deniedExtensions {
  274. if path.Clean(allowed.Path) == path.Clean(denied.Path) {
  275. allowed.DeniedExtensions = append(allowed.DeniedExtensions, denied.DeniedExtensions...)
  276. }
  277. }
  278. extensions = append(extensions, allowed)
  279. }
  280. for _, denied := range deniedExtensions {
  281. found := false
  282. for _, allowed := range allowedExtensions {
  283. if path.Clean(denied.Path) == path.Clean(allowed.Path) {
  284. found = true
  285. break
  286. }
  287. }
  288. if !found {
  289. extensions = append(extensions, denied)
  290. }
  291. }
  292. } else if len(allowedExtensions) > 0 {
  293. extensions = append(extensions, allowedExtensions...)
  294. } else if len(deniedExtensions) > 0 {
  295. extensions = append(extensions, deniedExtensions...)
  296. }
  297. filters.FileExtensions = extensions
  298. return filters
  299. }
  300. func getFsConfigFromUserPostFields(r *http.Request) (dataprovider.Filesystem, error) {
  301. var fs dataprovider.Filesystem
  302. provider, err := strconv.Atoi(r.Form.Get("fs_provider"))
  303. if err != nil {
  304. provider = 0
  305. }
  306. fs.Provider = provider
  307. if fs.Provider == 1 {
  308. fs.S3Config.Bucket = r.Form.Get("s3_bucket")
  309. fs.S3Config.Region = r.Form.Get("s3_region")
  310. fs.S3Config.AccessKey = r.Form.Get("s3_access_key")
  311. fs.S3Config.AccessSecret = r.Form.Get("s3_access_secret")
  312. fs.S3Config.Endpoint = r.Form.Get("s3_endpoint")
  313. fs.S3Config.StorageClass = r.Form.Get("s3_storage_class")
  314. fs.S3Config.KeyPrefix = r.Form.Get("s3_key_prefix")
  315. fs.S3Config.UploadPartSize, err = strconv.ParseInt(r.Form.Get("s3_upload_part_size"), 10, 64)
  316. if err != nil {
  317. return fs, err
  318. }
  319. fs.S3Config.UploadConcurrency, err = strconv.Atoi(r.Form.Get("s3_upload_concurrency"))
  320. if err != nil {
  321. return fs, err
  322. }
  323. } else if fs.Provider == 2 {
  324. fs.GCSConfig.Bucket = r.Form.Get("gcs_bucket")
  325. fs.GCSConfig.StorageClass = r.Form.Get("gcs_storage_class")
  326. fs.GCSConfig.KeyPrefix = r.Form.Get("gcs_key_prefix")
  327. autoCredentials := r.Form.Get("gcs_auto_credentials")
  328. if len(autoCredentials) > 0 {
  329. fs.GCSConfig.AutomaticCredentials = 1
  330. } else {
  331. fs.GCSConfig.AutomaticCredentials = 0
  332. }
  333. credentials, _, err := r.FormFile("gcs_credential_file")
  334. if err == http.ErrMissingFile {
  335. return fs, nil
  336. }
  337. if err != nil {
  338. return fs, err
  339. }
  340. defer credentials.Close()
  341. fileBytes, err := ioutil.ReadAll(credentials)
  342. if err != nil || len(fileBytes) == 0 {
  343. if len(fileBytes) == 0 {
  344. err = errors.New("credentials file size must be greater than 0")
  345. }
  346. return fs, err
  347. }
  348. fs.GCSConfig.Credentials = base64.StdEncoding.EncodeToString(fileBytes)
  349. fs.GCSConfig.AutomaticCredentials = 0
  350. }
  351. return fs, nil
  352. }
  353. func getUserFromPostFields(r *http.Request) (dataprovider.User, error) {
  354. var user dataprovider.User
  355. err := r.ParseMultipartForm(maxRequestSize)
  356. if err != nil {
  357. return user, err
  358. }
  359. publicKeysFormValue := r.Form.Get("public_keys")
  360. publicKeys := getSliceFromDelimitedValues(publicKeysFormValue, "\n")
  361. uid, err := strconv.Atoi(r.Form.Get("uid"))
  362. if err != nil {
  363. return user, err
  364. }
  365. gid, err := strconv.Atoi(r.Form.Get("gid"))
  366. if err != nil {
  367. return user, err
  368. }
  369. maxSessions, err := strconv.Atoi(r.Form.Get("max_sessions"))
  370. if err != nil {
  371. return user, err
  372. }
  373. quotaSize, err := strconv.ParseInt(r.Form.Get("quota_size"), 10, 64)
  374. if err != nil {
  375. return user, err
  376. }
  377. quotaFiles, err := strconv.Atoi(r.Form.Get("quota_files"))
  378. if err != nil {
  379. return user, err
  380. }
  381. bandwidthUL, err := strconv.ParseInt(r.Form.Get("upload_bandwidth"), 10, 64)
  382. if err != nil {
  383. return user, err
  384. }
  385. bandwidthDL, err := strconv.ParseInt(r.Form.Get("download_bandwidth"), 10, 64)
  386. if err != nil {
  387. return user, err
  388. }
  389. status, err := strconv.Atoi(r.Form.Get("status"))
  390. if err != nil {
  391. return user, err
  392. }
  393. expirationDateMillis := int64(0)
  394. expirationDateString := r.Form.Get("expiration_date")
  395. if len(strings.TrimSpace(expirationDateString)) > 0 {
  396. expirationDate, err := time.Parse(webDateTimeFormat, expirationDateString)
  397. if err != nil {
  398. return user, err
  399. }
  400. expirationDateMillis = utils.GetTimeAsMsSinceEpoch(expirationDate)
  401. }
  402. fsConfig, err := getFsConfigFromUserPostFields(r)
  403. if err != nil {
  404. return user, err
  405. }
  406. user = dataprovider.User{
  407. Username: r.Form.Get("username"),
  408. Password: r.Form.Get("password"),
  409. PublicKeys: publicKeys,
  410. HomeDir: r.Form.Get("home_dir"),
  411. VirtualFolders: getVirtualFoldersFromPostFields(r),
  412. UID: uid,
  413. GID: gid,
  414. Permissions: getUserPermissionsFromPostFields(r),
  415. MaxSessions: maxSessions,
  416. QuotaSize: quotaSize,
  417. QuotaFiles: quotaFiles,
  418. UploadBandwidth: bandwidthUL,
  419. DownloadBandwidth: bandwidthDL,
  420. Status: status,
  421. ExpirationDate: expirationDateMillis,
  422. Filters: getFiltersFromUserPostFields(r),
  423. FsConfig: fsConfig,
  424. }
  425. return user, err
  426. }
  427. func handleGetWebUsers(w http.ResponseWriter, r *http.Request) {
  428. limit := defaultUsersQueryLimit
  429. if _, ok := r.URL.Query()["qlimit"]; ok {
  430. var err error
  431. limit, err = strconv.Atoi(r.URL.Query().Get("qlimit"))
  432. if err != nil {
  433. limit = defaultUsersQueryLimit
  434. }
  435. }
  436. var users []dataprovider.User
  437. u, err := dataprovider.GetUsers(dataProvider, limit, 0, "ASC", "")
  438. users = append(users, u...)
  439. for len(u) == limit {
  440. u, err = dataprovider.GetUsers(dataProvider, limit, len(users), "ASC", "")
  441. if err == nil && len(u) > 0 {
  442. users = append(users, u...)
  443. } else {
  444. break
  445. }
  446. }
  447. if err != nil {
  448. renderInternalServerErrorPage(w, err)
  449. return
  450. }
  451. data := usersPage{
  452. basePage: getBasePageData(pageUsersTitle, webUsersPath),
  453. Users: users,
  454. }
  455. renderTemplate(w, templateUsers, data)
  456. }
  457. func handleWebAddUserGet(w http.ResponseWriter, r *http.Request) {
  458. renderAddUserPage(w, dataprovider.User{Status: 1}, "")
  459. }
  460. func handleWebUpdateUserGet(w http.ResponseWriter, r *http.Request) {
  461. id, err := strconv.ParseInt(chi.URLParam(r, "userID"), 10, 64)
  462. if err != nil {
  463. renderBadRequestPage(w, err)
  464. return
  465. }
  466. user, err := dataprovider.GetUserByID(dataProvider, id)
  467. if err == nil {
  468. renderUpdateUserPage(w, user, "")
  469. } else if _, ok := err.(*dataprovider.RecordNotFoundError); ok {
  470. renderNotFoundPage(w, err)
  471. } else {
  472. renderInternalServerErrorPage(w, err)
  473. }
  474. }
  475. func handleWebAddUserPost(w http.ResponseWriter, r *http.Request) {
  476. r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
  477. user, err := getUserFromPostFields(r)
  478. if err != nil {
  479. renderAddUserPage(w, user, err.Error())
  480. return
  481. }
  482. err = dataprovider.AddUser(dataProvider, user)
  483. if err == nil {
  484. http.Redirect(w, r, webUsersPath, http.StatusSeeOther)
  485. } else {
  486. renderAddUserPage(w, user, err.Error())
  487. }
  488. }
  489. func handleWebUpdateUserPost(w http.ResponseWriter, r *http.Request) {
  490. r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
  491. id, err := strconv.ParseInt(chi.URLParam(r, "userID"), 10, 64)
  492. if err != nil {
  493. renderBadRequestPage(w, err)
  494. return
  495. }
  496. user, err := dataprovider.GetUserByID(dataProvider, id)
  497. if _, ok := err.(*dataprovider.RecordNotFoundError); ok {
  498. renderNotFoundPage(w, err)
  499. return
  500. } else if err != nil {
  501. renderInternalServerErrorPage(w, err)
  502. return
  503. }
  504. updatedUser, err := getUserFromPostFields(r)
  505. if err != nil {
  506. renderUpdateUserPage(w, user, err.Error())
  507. return
  508. }
  509. updatedUser.ID = user.ID
  510. if len(updatedUser.Password) == 0 {
  511. updatedUser.Password = user.Password
  512. }
  513. err = dataprovider.UpdateUser(dataProvider, updatedUser)
  514. if err == nil {
  515. http.Redirect(w, r, webUsersPath, http.StatusSeeOther)
  516. } else {
  517. renderUpdateUserPage(w, user, err.Error())
  518. }
  519. }
  520. func handleWebGetConnections(w http.ResponseWriter, r *http.Request) {
  521. connectionStats := sftpd.GetConnectionsStats()
  522. data := connectionsPage{
  523. basePage: getBasePageData(pageConnectionsTitle, webConnectionsPath),
  524. Connections: connectionStats,
  525. }
  526. renderTemplate(w, templateConnections, data)
  527. }