123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671 |
- package httpd
- import (
- "errors"
- "fmt"
- "html/template"
- "io/ioutil"
- "net/http"
- "path"
- "path/filepath"
- "strconv"
- "strings"
- "time"
- "github.com/go-chi/chi"
- "github.com/drakkan/sftpgo/common"
- "github.com/drakkan/sftpgo/dataprovider"
- "github.com/drakkan/sftpgo/utils"
- "github.com/drakkan/sftpgo/version"
- "github.com/drakkan/sftpgo/vfs"
- )
- const (
- templateBase = "base.html"
- templateUsers = "users.html"
- templateUser = "user.html"
- templateConnections = "connections.html"
- templateFolders = "folders.html"
- templateFolder = "folder.html"
- templateMessage = "message.html"
- pageUsersTitle = "Users"
- pageConnectionsTitle = "Connections"
- pageFoldersTitle = "Folders"
- page400Title = "Bad request"
- page404Title = "Not found"
- page404Body = "The page you are looking for does not exist."
- page500Title = "Internal Server Error"
- page500Body = "The server is unable to fulfill your request."
- defaultQueryLimit = 500
- webDateTimeFormat = "2006-01-02 15:04:05" // YYYY-MM-DD HH:MM:SS
- )
- var (
- templates = make(map[string]*template.Template)
- )
- type basePage struct {
- Title string
- CurrentURL string
- UsersURL string
- UserURL string
- APIUserURL string
- APIConnectionsURL string
- APIQuotaScanURL string
- ConnectionsURL string
- FoldersURL string
- FolderURL string
- APIFoldersURL string
- APIFolderQuotaScanURL string
- UsersTitle string
- ConnectionsTitle string
- FoldersTitle string
- Version string
- }
- type usersPage struct {
- basePage
- Users []dataprovider.User
- }
- type foldersPage struct {
- basePage
- Folders []vfs.BaseVirtualFolder
- }
- type connectionsPage struct {
- basePage
- Connections []common.ConnectionStatus
- }
- type userPage struct {
- basePage
- IsAdd bool
- User dataprovider.User
- RootPerms []string
- Error string
- ValidPerms []string
- ValidSSHLoginMethods []string
- ValidProtocols []string
- RootDirPerms []string
- }
- type folderPage struct {
- basePage
- Folder vfs.BaseVirtualFolder
- Error string
- }
- type messagePage struct {
- basePage
- Error string
- Success string
- }
- func loadTemplates(templatesPath string) {
- usersPaths := []string{
- filepath.Join(templatesPath, templateBase),
- filepath.Join(templatesPath, templateUsers),
- }
- userPaths := []string{
- filepath.Join(templatesPath, templateBase),
- filepath.Join(templatesPath, templateUser),
- }
- connectionsPaths := []string{
- filepath.Join(templatesPath, templateBase),
- filepath.Join(templatesPath, templateConnections),
- }
- messagePath := []string{
- filepath.Join(templatesPath, templateBase),
- filepath.Join(templatesPath, templateMessage),
- }
- foldersPath := []string{
- filepath.Join(templatesPath, templateBase),
- filepath.Join(templatesPath, templateFolders),
- }
- folderPath := []string{
- filepath.Join(templatesPath, templateBase),
- filepath.Join(templatesPath, templateFolder),
- }
- usersTmpl := utils.LoadTemplate(template.ParseFiles(usersPaths...))
- userTmpl := utils.LoadTemplate(template.ParseFiles(userPaths...))
- connectionsTmpl := utils.LoadTemplate(template.ParseFiles(connectionsPaths...))
- messageTmpl := utils.LoadTemplate(template.ParseFiles(messagePath...))
- foldersTmpl := utils.LoadTemplate(template.ParseFiles(foldersPath...))
- folderTmpl := utils.LoadTemplate(template.ParseFiles(folderPath...))
- templates[templateUsers] = usersTmpl
- templates[templateUser] = userTmpl
- templates[templateConnections] = connectionsTmpl
- templates[templateMessage] = messageTmpl
- templates[templateFolders] = foldersTmpl
- templates[templateFolder] = folderTmpl
- }
- func getBasePageData(title, currentURL string) basePage {
- return basePage{
- Title: title,
- CurrentURL: currentURL,
- UsersURL: webUsersPath,
- UserURL: webUserPath,
- FoldersURL: webFoldersPath,
- FolderURL: webFolderPath,
- APIUserURL: userPath,
- APIConnectionsURL: activeConnectionsPath,
- APIQuotaScanURL: quotaScanPath,
- APIFoldersURL: folderPath,
- APIFolderQuotaScanURL: quotaScanVFolderPath,
- ConnectionsURL: webConnectionsPath,
- UsersTitle: pageUsersTitle,
- ConnectionsTitle: pageConnectionsTitle,
- FoldersTitle: pageFoldersTitle,
- Version: version.GetAsString(),
- }
- }
- func renderTemplate(w http.ResponseWriter, tmplName string, data interface{}) {
- err := templates[tmplName].ExecuteTemplate(w, tmplName, data)
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- }
- }
- func renderMessagePage(w http.ResponseWriter, title, body string, statusCode int, err error, message string) {
- var errorString string
- if len(body) > 0 {
- errorString = body + " "
- }
- if err != nil {
- errorString += err.Error()
- }
- data := messagePage{
- basePage: getBasePageData(title, ""),
- Error: errorString,
- Success: message,
- }
- w.WriteHeader(statusCode)
- renderTemplate(w, templateMessage, data)
- }
- func renderInternalServerErrorPage(w http.ResponseWriter, err error) {
- renderMessagePage(w, page500Title, page500Body, http.StatusInternalServerError, err, "")
- }
- func renderBadRequestPage(w http.ResponseWriter, err error) {
- renderMessagePage(w, page400Title, "", http.StatusBadRequest, err, "")
- }
- func renderNotFoundPage(w http.ResponseWriter, err error) {
- renderMessagePage(w, page404Title, page404Body, http.StatusNotFound, err, "")
- }
- func renderAddUserPage(w http.ResponseWriter, user dataprovider.User, error string) {
- data := userPage{
- basePage: getBasePageData("Add a new user", webUserPath),
- IsAdd: true,
- Error: error,
- User: user,
- ValidPerms: dataprovider.ValidPerms,
- ValidSSHLoginMethods: dataprovider.ValidSSHLoginMethods,
- ValidProtocols: dataprovider.ValidProtocols,
- RootDirPerms: user.GetPermissionsForPath("/"),
- }
- renderTemplate(w, templateUser, data)
- }
- func renderUpdateUserPage(w http.ResponseWriter, user dataprovider.User, error string) {
- data := userPage{
- basePage: getBasePageData("Update user", fmt.Sprintf("%v/%v", webUserPath, user.ID)),
- IsAdd: false,
- Error: error,
- User: user,
- ValidPerms: dataprovider.ValidPerms,
- ValidSSHLoginMethods: dataprovider.ValidSSHLoginMethods,
- ValidProtocols: dataprovider.ValidProtocols,
- RootDirPerms: user.GetPermissionsForPath("/"),
- }
- renderTemplate(w, templateUser, data)
- }
- func renderAddFolderPage(w http.ResponseWriter, folder vfs.BaseVirtualFolder, error string) {
- data := folderPage{
- basePage: getBasePageData("Add a new folder", webFolderPath),
- Error: error,
- Folder: folder,
- }
- renderTemplate(w, templateFolder, data)
- }
- func getVirtualFoldersFromPostFields(r *http.Request) []vfs.VirtualFolder {
- var virtualFolders []vfs.VirtualFolder
- formValue := r.Form.Get("virtual_folders")
- for _, cleaned := range getSliceFromDelimitedValues(formValue, "\n") {
- if strings.Contains(cleaned, "::") {
- mapping := strings.Split(cleaned, "::")
- if len(mapping) > 1 {
- vfolder := vfs.VirtualFolder{
- BaseVirtualFolder: vfs.BaseVirtualFolder{
- MappedPath: strings.TrimSpace(mapping[1]),
- },
- VirtualPath: strings.TrimSpace(mapping[0]),
- QuotaFiles: -1,
- QuotaSize: -1,
- }
- if len(mapping) > 2 {
- quotaFiles, err := strconv.Atoi(strings.TrimSpace(mapping[2]))
- if err == nil {
- vfolder.QuotaFiles = quotaFiles
- }
- }
- if len(mapping) > 3 {
- quotaSize, err := strconv.ParseInt(strings.TrimSpace(mapping[3]), 10, 64)
- if err == nil {
- vfolder.QuotaSize = quotaSize
- }
- }
- virtualFolders = append(virtualFolders, vfolder)
- }
- }
- }
- return virtualFolders
- }
- func getUserPermissionsFromPostFields(r *http.Request) map[string][]string {
- permissions := make(map[string][]string)
- permissions["/"] = r.Form["permissions"]
- subDirsPermsValue := r.Form.Get("sub_dirs_permissions")
- for _, cleaned := range getSliceFromDelimitedValues(subDirsPermsValue, "\n") {
- if strings.Contains(cleaned, "::") {
- dirPerms := strings.Split(cleaned, "::")
- if len(dirPerms) > 1 {
- dir := dirPerms[0]
- dir = strings.TrimSpace(dir)
- perms := []string{}
- for _, p := range strings.Split(dirPerms[1], ",") {
- cleanedPerm := strings.TrimSpace(p)
- if len(cleanedPerm) > 0 {
- perms = append(perms, cleanedPerm)
- }
- }
- if len(dir) > 0 {
- permissions[dir] = perms
- }
- }
- }
- }
- return permissions
- }
- func getSliceFromDelimitedValues(values, delimiter string) []string {
- result := []string{}
- for _, v := range strings.Split(values, delimiter) {
- cleaned := strings.TrimSpace(v)
- if len(cleaned) > 0 {
- result = append(result, cleaned)
- }
- }
- return result
- }
- func getFileExtensionsFromPostField(value string, extesionsType int) []dataprovider.ExtensionsFilter {
- var result []dataprovider.ExtensionsFilter
- for _, cleaned := range getSliceFromDelimitedValues(value, "\n") {
- if strings.Contains(cleaned, "::") {
- dirExts := strings.Split(cleaned, "::")
- if len(dirExts) > 1 {
- dir := dirExts[0]
- dir = strings.TrimSpace(dir)
- exts := []string{}
- for _, e := range strings.Split(dirExts[1], ",") {
- cleanedExt := strings.TrimSpace(e)
- if len(cleanedExt) > 0 {
- exts = append(exts, cleanedExt)
- }
- }
- if len(dir) > 0 {
- filter := dataprovider.ExtensionsFilter{
- Path: dir,
- }
- if extesionsType == 1 {
- filter.AllowedExtensions = exts
- filter.DeniedExtensions = []string{}
- } else {
- filter.DeniedExtensions = exts
- filter.AllowedExtensions = []string{}
- }
- result = append(result, filter)
- }
- }
- }
- }
- return result
- }
- func getFiltersFromUserPostFields(r *http.Request) dataprovider.UserFilters {
- var filters dataprovider.UserFilters
- filters.AllowedIP = getSliceFromDelimitedValues(r.Form.Get("allowed_ip"), ",")
- filters.DeniedIP = getSliceFromDelimitedValues(r.Form.Get("denied_ip"), ",")
- filters.DeniedLoginMethods = r.Form["ssh_login_methods"]
- filters.DeniedProtocols = r.Form["denied_protocols"]
- allowedExtensions := getFileExtensionsFromPostField(r.Form.Get("allowed_extensions"), 1)
- deniedExtensions := getFileExtensionsFromPostField(r.Form.Get("denied_extensions"), 2)
- extensions := []dataprovider.ExtensionsFilter{}
- if len(allowedExtensions) > 0 && len(deniedExtensions) > 0 {
- for _, allowed := range allowedExtensions {
- for _, denied := range deniedExtensions {
- if path.Clean(allowed.Path) == path.Clean(denied.Path) {
- allowed.DeniedExtensions = append(allowed.DeniedExtensions, denied.DeniedExtensions...)
- }
- }
- extensions = append(extensions, allowed)
- }
- for _, denied := range deniedExtensions {
- found := false
- for _, allowed := range allowedExtensions {
- if path.Clean(denied.Path) == path.Clean(allowed.Path) {
- found = true
- break
- }
- }
- if !found {
- extensions = append(extensions, denied)
- }
- }
- } else if len(allowedExtensions) > 0 {
- extensions = append(extensions, allowedExtensions...)
- } else if len(deniedExtensions) > 0 {
- extensions = append(extensions, deniedExtensions...)
- }
- filters.FileExtensions = extensions
- return filters
- }
- func getFsConfigFromUserPostFields(r *http.Request) (dataprovider.Filesystem, error) {
- var fs dataprovider.Filesystem
- provider, err := strconv.Atoi(r.Form.Get("fs_provider"))
- if err != nil {
- provider = int(dataprovider.LocalFilesystemProvider)
- }
- fs.Provider = dataprovider.FilesystemProvider(provider)
- if fs.Provider == dataprovider.S3FilesystemProvider {
- fs.S3Config.Bucket = r.Form.Get("s3_bucket")
- fs.S3Config.Region = r.Form.Get("s3_region")
- fs.S3Config.AccessKey = r.Form.Get("s3_access_key")
- fs.S3Config.AccessSecret = r.Form.Get("s3_access_secret")
- fs.S3Config.Endpoint = r.Form.Get("s3_endpoint")
- fs.S3Config.StorageClass = r.Form.Get("s3_storage_class")
- fs.S3Config.KeyPrefix = r.Form.Get("s3_key_prefix")
- fs.S3Config.UploadPartSize, err = strconv.ParseInt(r.Form.Get("s3_upload_part_size"), 10, 64)
- if err != nil {
- return fs, err
- }
- fs.S3Config.UploadConcurrency, err = strconv.Atoi(r.Form.Get("s3_upload_concurrency"))
- if err != nil {
- return fs, err
- }
- } else if fs.Provider == dataprovider.GCSFilesystemProvider {
- fs.GCSConfig.Bucket = r.Form.Get("gcs_bucket")
- fs.GCSConfig.StorageClass = r.Form.Get("gcs_storage_class")
- fs.GCSConfig.KeyPrefix = r.Form.Get("gcs_key_prefix")
- autoCredentials := r.Form.Get("gcs_auto_credentials")
- if len(autoCredentials) > 0 {
- fs.GCSConfig.AutomaticCredentials = 1
- } else {
- fs.GCSConfig.AutomaticCredentials = 0
- }
- credentials, _, err := r.FormFile("gcs_credential_file")
- if err == http.ErrMissingFile {
- return fs, nil
- }
- if err != nil {
- return fs, err
- }
- defer credentials.Close()
- fileBytes, err := ioutil.ReadAll(credentials)
- if err != nil || len(fileBytes) == 0 {
- if len(fileBytes) == 0 {
- err = errors.New("credentials file size must be greater than 0")
- }
- return fs, err
- }
- fs.GCSConfig.Credentials = fileBytes
- fs.GCSConfig.AutomaticCredentials = 0
- }
- return fs, nil
- }
- func getUserFromPostFields(r *http.Request) (dataprovider.User, error) {
- var user dataprovider.User
- err := r.ParseMultipartForm(maxRequestSize)
- if err != nil {
- return user, err
- }
- publicKeysFormValue := r.Form.Get("public_keys")
- publicKeys := getSliceFromDelimitedValues(publicKeysFormValue, "\n")
- uid, err := strconv.Atoi(r.Form.Get("uid"))
- if err != nil {
- return user, err
- }
- gid, err := strconv.Atoi(r.Form.Get("gid"))
- if err != nil {
- return user, err
- }
- maxSessions, err := strconv.Atoi(r.Form.Get("max_sessions"))
- if err != nil {
- return user, err
- }
- quotaSize, err := strconv.ParseInt(r.Form.Get("quota_size"), 10, 64)
- if err != nil {
- return user, err
- }
- quotaFiles, err := strconv.Atoi(r.Form.Get("quota_files"))
- if err != nil {
- return user, err
- }
- bandwidthUL, err := strconv.ParseInt(r.Form.Get("upload_bandwidth"), 10, 64)
- if err != nil {
- return user, err
- }
- bandwidthDL, err := strconv.ParseInt(r.Form.Get("download_bandwidth"), 10, 64)
- if err != nil {
- return user, err
- }
- status, err := strconv.Atoi(r.Form.Get("status"))
- if err != nil {
- return user, err
- }
- expirationDateMillis := int64(0)
- expirationDateString := r.Form.Get("expiration_date")
- if len(strings.TrimSpace(expirationDateString)) > 0 {
- expirationDate, err := time.Parse(webDateTimeFormat, expirationDateString)
- if err != nil {
- return user, err
- }
- expirationDateMillis = utils.GetTimeAsMsSinceEpoch(expirationDate)
- }
- fsConfig, err := getFsConfigFromUserPostFields(r)
- if err != nil {
- return user, err
- }
- user = dataprovider.User{
- Username: r.Form.Get("username"),
- Password: r.Form.Get("password"),
- PublicKeys: publicKeys,
- HomeDir: r.Form.Get("home_dir"),
- VirtualFolders: getVirtualFoldersFromPostFields(r),
- UID: uid,
- GID: gid,
- Permissions: getUserPermissionsFromPostFields(r),
- MaxSessions: maxSessions,
- QuotaSize: quotaSize,
- QuotaFiles: quotaFiles,
- UploadBandwidth: bandwidthUL,
- DownloadBandwidth: bandwidthDL,
- Status: status,
- ExpirationDate: expirationDateMillis,
- Filters: getFiltersFromUserPostFields(r),
- FsConfig: fsConfig,
- }
- maxFileSize, err := strconv.ParseInt(r.Form.Get("max_upload_file_size"), 10, 64)
- user.Filters.MaxUploadFileSize = maxFileSize
- return user, err
- }
- func handleGetWebUsers(w http.ResponseWriter, r *http.Request) {
- limit := defaultQueryLimit
- if _, ok := r.URL.Query()["qlimit"]; ok {
- var err error
- limit, err = strconv.Atoi(r.URL.Query().Get("qlimit"))
- if err != nil {
- limit = defaultQueryLimit
- }
- }
- users := make([]dataprovider.User, 0, limit)
- for {
- u, err := dataprovider.GetUsers(limit, len(users), dataprovider.OrderASC, "")
- if err != nil {
- renderInternalServerErrorPage(w, err)
- return
- }
- users = append(users, u...)
- if len(u) < limit {
- break
- }
- }
- data := usersPage{
- basePage: getBasePageData(pageUsersTitle, webUsersPath),
- Users: users,
- }
- renderTemplate(w, templateUsers, data)
- }
- func handleWebAddUserGet(w http.ResponseWriter, r *http.Request) {
- renderAddUserPage(w, dataprovider.User{Status: 1}, "")
- }
- func handleWebUpdateUserGet(w http.ResponseWriter, r *http.Request) {
- id, err := strconv.ParseInt(chi.URLParam(r, "userID"), 10, 64)
- if err != nil {
- renderBadRequestPage(w, err)
- return
- }
- user, err := dataprovider.GetUserByID(id)
- if err == nil {
- renderUpdateUserPage(w, user, "")
- } else if _, ok := err.(*dataprovider.RecordNotFoundError); ok {
- renderNotFoundPage(w, err)
- } else {
- renderInternalServerErrorPage(w, err)
- }
- }
- func handleWebAddUserPost(w http.ResponseWriter, r *http.Request) {
- r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
- user, err := getUserFromPostFields(r)
- if err != nil {
- renderAddUserPage(w, user, err.Error())
- return
- }
- err = dataprovider.AddUser(user)
- if err == nil {
- http.Redirect(w, r, webUsersPath, http.StatusSeeOther)
- } else {
- renderAddUserPage(w, user, err.Error())
- }
- }
- func handleWebUpdateUserPost(w http.ResponseWriter, r *http.Request) {
- r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
- id, err := strconv.ParseInt(chi.URLParam(r, "userID"), 10, 64)
- if err != nil {
- renderBadRequestPage(w, err)
- return
- }
- user, err := dataprovider.GetUserByID(id)
- if _, ok := err.(*dataprovider.RecordNotFoundError); ok {
- renderNotFoundPage(w, err)
- return
- } else if err != nil {
- renderInternalServerErrorPage(w, err)
- return
- }
- updatedUser, err := getUserFromPostFields(r)
- if err != nil {
- renderUpdateUserPage(w, user, err.Error())
- return
- }
- updatedUser.ID = user.ID
- if len(updatedUser.Password) == 0 {
- updatedUser.Password = user.Password
- }
- err = dataprovider.UpdateUser(updatedUser)
- if err == nil {
- if len(r.Form.Get("disconnect")) > 0 {
- disconnectUser(user.Username)
- }
- http.Redirect(w, r, webUsersPath, http.StatusSeeOther)
- } else {
- renderUpdateUserPage(w, user, err.Error())
- }
- }
- func handleWebGetConnections(w http.ResponseWriter, r *http.Request) {
- connectionStats := common.Connections.GetStats()
- data := connectionsPage{
- basePage: getBasePageData(pageConnectionsTitle, webConnectionsPath),
- Connections: connectionStats,
- }
- renderTemplate(w, templateConnections, data)
- }
- func handleWebAddFolderGet(w http.ResponseWriter, r *http.Request) {
- renderAddFolderPage(w, vfs.BaseVirtualFolder{}, "")
- }
- func handleWebAddFolderPost(w http.ResponseWriter, r *http.Request) {
- r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
- folder := vfs.BaseVirtualFolder{}
- err := r.ParseForm()
- if err != nil {
- renderAddFolderPage(w, folder, err.Error())
- return
- }
- folder.MappedPath = r.Form.Get("mapped_path")
- err = dataprovider.AddFolder(folder)
- if err == nil {
- http.Redirect(w, r, webFoldersPath, http.StatusSeeOther)
- } else {
- renderAddFolderPage(w, folder, err.Error())
- }
- }
- func handleWebGetFolders(w http.ResponseWriter, r *http.Request) {
- limit := defaultQueryLimit
- if _, ok := r.URL.Query()["qlimit"]; ok {
- var err error
- limit, err = strconv.Atoi(r.URL.Query().Get("qlimit"))
- if err != nil {
- limit = defaultQueryLimit
- }
- }
- folders := make([]vfs.BaseVirtualFolder, 0, limit)
- for {
- f, err := dataprovider.GetFolders(limit, len(folders), dataprovider.OrderASC, "")
- if err != nil {
- renderInternalServerErrorPage(w, err)
- return
- }
- folders = append(folders, f...)
- if len(f) < limit {
- break
- }
- }
- data := foldersPage{
- basePage: getBasePageData(pageFoldersTitle, webFoldersPath),
- Folders: folders,
- }
- renderTemplate(w, templateFolders, data)
- }
|